@ours.network/fleet 1.2.0-nightly.10 → 1.2.0-nightly.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/application/task-room-service.d.ts +6 -2
- package/dist/application/task-room-service.js +23 -2
- package/dist/build-info.json +4 -4
- package/dist/owner-channel/channel.js +9 -2
- package/dist/owner-channel/commands.js +5 -0
- package/dist/rooms-tasks/cli.js +5 -0
- package/dist/rooms-tasks/live-readiness.d.ts +18 -0
- package/dist/rooms-tasks/live-readiness.js +75 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1752,3 +1752,18 @@ or message delivery. File tools resolve paths in the invoking CLI process or
|
|
|
1752
1752
|
Fleet web server's filesystem context, with that process's access permissions.
|
|
1753
1753
|
|
|
1754
1754
|
Task workspace ownership, retention, safe deletion and legacy migration are documented in [Task workspaces](docs/task-workspaces.md).
|
|
1755
|
+
|
|
1756
|
+
### Active task readiness after interruption
|
|
1757
|
+
|
|
1758
|
+
Task and room list states, and `taskProvisioningOutcome`, describe durable lifecycle
|
|
1759
|
+
and provisioning progress; they are not live health checks. Retrying `task start`
|
|
1760
|
+
or `task work` on an active task checks the current Cowork room identity and seats,
|
|
1761
|
+
member launch ownership, supervisor liveness, and authenticated session status
|
|
1762
|
+
before reporting success. Waiting for provisioning completion also checks live
|
|
1763
|
+
readiness. This is a point-in-time observation, not proof of an agent-authored reply.
|
|
1764
|
+
|
|
1765
|
+
A `task_not_ready` result distinguishes confirmed degradation from unknown health
|
|
1766
|
+
(for example, an unavailable control socket). Ask Fleet Coordinator to inspect room
|
|
1767
|
+
and member status and choose supported recovery. These checks do not restart agents,
|
|
1768
|
+
recreate identities, redeem invites, or rewrite lifecycle state. Temporary room agents
|
|
1769
|
+
remain transient across host reboot; retained launch records do not restore them.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type LiveReadinessDeps } from '../rooms-tasks/live-readiness.js';
|
|
1
2
|
import { type FleetConfig } from '../config.js';
|
|
2
3
|
import { type CoworkAdapter } from '../rooms-tasks/cowork-adapter.js';
|
|
3
4
|
import { provisionMembers } from '../rooms-tasks/provision.js';
|
|
@@ -57,9 +58,9 @@ export interface TaskProvisioningOutcome {
|
|
|
57
58
|
/** Repairable terminal observation derived entirely from durable Task/Room state. */
|
|
58
59
|
export declare function recordTaskProvisioningOutcome(outcome: TaskProvisioningOutcome): void;
|
|
59
60
|
export declare class TaskRoomApplicationError extends Error {
|
|
60
|
-
readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found';
|
|
61
|
+
readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found' | 'task_not_ready';
|
|
61
62
|
readonly fields: Readonly<Record<string, string>>;
|
|
62
|
-
constructor(code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found', message: string, fields?: Readonly<Record<string, string>>);
|
|
63
|
+
constructor(code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found' | 'task_not_ready', message: string, fields?: Readonly<Record<string, string>>);
|
|
63
64
|
}
|
|
64
65
|
export interface CreateTaskRequest {
|
|
65
66
|
actor: TaskRoomActor;
|
|
@@ -87,6 +88,7 @@ export interface CreateRoomRequest {
|
|
|
87
88
|
}
|
|
88
89
|
export declare function resolveRoomLaunchPolicy(template: TemplateSnapshot | undefined, override: boolean | undefined): RoomLaunchPolicy;
|
|
89
90
|
export interface TaskRoomServiceDeps {
|
|
91
|
+
liveReadiness?: LiveReadinessDeps;
|
|
90
92
|
loadConfiguration?(path?: string): FleetConfig;
|
|
91
93
|
cowork?(config: FleetConfig): CoworkAdapter;
|
|
92
94
|
binPath?(): string;
|
|
@@ -151,6 +153,7 @@ export declare class TaskRoomApplicationService {
|
|
|
151
153
|
task: TaskRecord;
|
|
152
154
|
orchestration: RoomOrchestrationRecord | undefined;
|
|
153
155
|
};
|
|
156
|
+
/** Durable provisioning progress only; use active task start/work for a live readiness check. */
|
|
154
157
|
taskProvisioningOutcome(taskId: string): TaskProvisioningOutcome;
|
|
155
158
|
awaitTaskProvisioning(input: {
|
|
156
159
|
actor: TaskRoomActor;
|
|
@@ -300,6 +303,7 @@ export declare class TaskRoomApplicationService {
|
|
|
300
303
|
taskId: string;
|
|
301
304
|
outcome?: TaskOutcome;
|
|
302
305
|
}): Promise<TaskSettlementPlan>;
|
|
306
|
+
private assertTaskLiveReadiness;
|
|
303
307
|
ensureTaskWork(input: {
|
|
304
308
|
actor: TaskRoomActor;
|
|
305
309
|
taskId: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { taskLiveReadiness } from '../rooms-tasks/live-readiness.js';
|
|
2
3
|
import { ConfigError, loadConfig } from '../config.js';
|
|
3
4
|
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
4
5
|
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
@@ -223,6 +224,7 @@ export class TaskRoomApplicationService {
|
|
|
223
224
|
const task = readTask(taskId);
|
|
224
225
|
return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
|
|
225
226
|
}
|
|
227
|
+
/** Durable provisioning progress only; use active task start/work for a live readiness check. */
|
|
226
228
|
taskProvisioningOutcome(taskId) {
|
|
227
229
|
const task = readTask(taskId);
|
|
228
230
|
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
@@ -271,6 +273,10 @@ export class TaskRoomApplicationService {
|
|
|
271
273
|
const deadline = now() + (input.waitMs ?? 60_000);
|
|
272
274
|
for (;;) {
|
|
273
275
|
const outcome = this.taskProvisioningOutcome(input.taskId);
|
|
276
|
+
if (outcome.kind === 'ready') {
|
|
277
|
+
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
278
|
+
await this.assertTaskLiveReadiness(outcome.task, outcome.room, cfg);
|
|
279
|
+
}
|
|
274
280
|
if (outcome.kind !== 'in_progress' || now() >= deadline) {
|
|
275
281
|
recordTaskProvisioningOutcome(outcome);
|
|
276
282
|
return outcome;
|
|
@@ -657,6 +663,14 @@ export class TaskRoomApplicationService {
|
|
|
657
663
|
(this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
658
664
|
return this.acceptTerminal(task.task_id, 'done', task.room_id, input.outcome);
|
|
659
665
|
}
|
|
666
|
+
async assertTaskLiveReadiness(task, room, cfg) {
|
|
667
|
+
const cowork = this.deps.cowork ? this.deps.cowork(cfg)
|
|
668
|
+
: createCoworkAdapter({ configPath: cfg.rooms?.cowork?.config });
|
|
669
|
+
const issue = await taskLiveReadiness(task, room, cowork, this.deps.liveReadiness);
|
|
670
|
+
if (issue)
|
|
671
|
+
throw new TaskRoomApplicationError('task_not_ready', `Task readiness is ${issue.state}: ${issue.reason}. Ask Fleet Coordinator to inspect the room and member status and choose supported recovery; do not blindly relaunch or reuse invites.`, { task: task.task_id, room: task.room_id ?? 'missing', readiness: issue.state,
|
|
672
|
+
reason: issue.reason, ...(issue.member ? { member: issue.member } : {}) });
|
|
673
|
+
}
|
|
660
674
|
async ensureTaskWork(input) {
|
|
661
675
|
const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
|
|
662
676
|
let task = readTask(input.taskId);
|
|
@@ -681,8 +695,10 @@ export class TaskRoomApplicationService {
|
|
|
681
695
|
const pinned = durableTemplate;
|
|
682
696
|
if (!pinned)
|
|
683
697
|
throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
|
|
684
|
-
if (durableRequestMatches)
|
|
698
|
+
if (durableRequestMatches) {
|
|
699
|
+
await this.assertTaskLiveReadiness(task, recordedRoom, cfg);
|
|
685
700
|
return { task, status: 'already_active' };
|
|
701
|
+
}
|
|
686
702
|
const templateName = input.template ?? pinned.name;
|
|
687
703
|
const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
|
|
688
704
|
if (!definition)
|
|
@@ -695,6 +711,7 @@ export class TaskRoomApplicationService {
|
|
|
695
711
|
provisioned: `${pinned.name}@${pinned.version}`
|
|
696
712
|
});
|
|
697
713
|
}
|
|
714
|
+
await this.assertTaskLiveReadiness(task, recordedRoom, cfg);
|
|
698
715
|
return { task, status: 'already_active' };
|
|
699
716
|
}
|
|
700
717
|
let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
@@ -833,7 +850,11 @@ export class TaskRoomApplicationService {
|
|
|
833
850
|
}
|
|
834
851
|
}
|
|
835
852
|
const resultingRoom = getRoomRecord(task.room_id);
|
|
836
|
-
|
|
853
|
+
if (task.state === 'active' && resultingRoom?.state === 'active') {
|
|
854
|
+
await this.assertTaskLiveReadiness(task, resultingRoom, cfg);
|
|
855
|
+
return { task, status: 'ready' };
|
|
856
|
+
}
|
|
857
|
+
return { task, status: 'in_progress' };
|
|
837
858
|
}
|
|
838
859
|
createTemplate(cfg, requested, noRoom) {
|
|
839
860
|
if (noRoom)
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.12",
|
|
3
|
+
"buildId": "6f07096c9a0c",
|
|
4
|
+
"commit": "7e323845714c5b638db197673b84f28fe00a2f75",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-24T13:14:00.219Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"cowork.http-management-v1",
|
|
9
9
|
"monitor.interrupt.after_tool"
|
|
@@ -883,8 +883,15 @@ export class OwnerChannel {
|
|
|
883
883
|
const recovered = [];
|
|
884
884
|
for (const claim of this.messageRecovery.list()) {
|
|
885
885
|
const item = await this.client.getHistoryItem(claim.wireId);
|
|
886
|
-
if (!item)
|
|
887
|
-
|
|
886
|
+
if (!item) {
|
|
887
|
+
// The daemon has definitively lost this body. Retrying the claim would
|
|
888
|
+
// block every later owner message, including after a supervisor restart.
|
|
889
|
+
// Keep transient lookup errors retryable; only an explicit absence skips.
|
|
890
|
+
this.messageRecovery.pruneHandled(wireId => wireId === claim.wireId);
|
|
891
|
+
this.options.log(`[${this.options.role}] skipped journaled owner message ${claim.wireId} `
|
|
892
|
+
+ `(seq=${claim.seq}): missing from persistent history`);
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
888
895
|
recovered.push(this.historyMessage(item, claim));
|
|
889
896
|
}
|
|
890
897
|
const listed = await this.client.listIncomingMessages();
|
|
@@ -47,6 +47,11 @@ function isKnownOwnerRoomState(message) {
|
|
|
47
47
|
].some(pattern => pattern.test(message));
|
|
48
48
|
}
|
|
49
49
|
function ownerTaskFailure(error) {
|
|
50
|
+
if (error instanceof TaskRoomApplicationError && error.code === 'task_not_ready')
|
|
51
|
+
return {
|
|
52
|
+
kind: 'state', detail: error.message,
|
|
53
|
+
action: 'Ask Fleet Coordinator to inspect room and member status and choose supported recovery.',
|
|
54
|
+
};
|
|
50
55
|
if (error instanceof TaskListError)
|
|
51
56
|
return {
|
|
52
57
|
kind: error.code === 'list_not_found' ? 'not_found'
|
package/dist/rooms-tasks/cli.js
CHANGED
|
@@ -75,6 +75,11 @@ async function presentDetachedProvisioningOutcome(outcome) {
|
|
|
75
75
|
function taskRoomPublicFailure(error) {
|
|
76
76
|
const f = error.fields;
|
|
77
77
|
switch (error.code) {
|
|
78
|
+
case 'task_not_ready': return {
|
|
79
|
+
legacy: `task readiness ${f.readiness}: ${f.reason}`, kind: 'state',
|
|
80
|
+
detail: `Task ${f.task} readiness is ${f.readiness}: ${f.reason}${f.member ? ` (${f.member})` : ''}.`,
|
|
81
|
+
action: 'Ask Fleet Coordinator to inspect room and member status and choose supported recovery. Do not blindly relaunch agents or reuse invites.',
|
|
82
|
+
};
|
|
78
83
|
case 'task_confirmation_mismatch': return {
|
|
79
84
|
legacy: 'confirmation ID must match task ID', kind: 'usage',
|
|
80
85
|
detail: 'The two task IDs must match.', action: 'Repeat the same task ID twice.',
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readRoomReadiness } from '../agent-ours/service.js';
|
|
2
|
+
import { controlRequest } from '../session/control.js';
|
|
3
|
+
import { readTempSupervisor, tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
4
|
+
import type { CoworkAdapter } from './cowork-adapter.js';
|
|
5
|
+
import type { RoomOrchestrationRecord, TaskRecord } from './types.js';
|
|
6
|
+
export type TaskReadinessIssue = {
|
|
7
|
+
state: 'degraded' | 'unknown';
|
|
8
|
+
reason: string;
|
|
9
|
+
member?: string;
|
|
10
|
+
};
|
|
11
|
+
export interface LiveReadinessDeps {
|
|
12
|
+
supervisor?: typeof readTempSupervisor;
|
|
13
|
+
liveness?: typeof tempSupervisorLiveness;
|
|
14
|
+
readiness?: typeof readRoomReadiness;
|
|
15
|
+
control?: typeof controlRequest;
|
|
16
|
+
}
|
|
17
|
+
/** Read-only observation. A failed probe never grants authority to replace a seat. */
|
|
18
|
+
export declare function taskLiveReadiness(task: TaskRecord, room: RoomOrchestrationRecord | undefined, cowork: Pick<CoworkAdapter, 'getRoom'>, deps?: LiveReadinessDeps): Promise<TaskReadinessIssue | undefined>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { realExec } from '../exec.js';
|
|
2
|
+
import { agentDir } from '../paths.js';
|
|
3
|
+
import { readRoomReadiness } from '../agent-ours/service.js';
|
|
4
|
+
import { controlRequest } from '../session/control.js';
|
|
5
|
+
import { readTempSupervisor, tempSupervisorLiveness } from '../temp-lifecycle.js';
|
|
6
|
+
/** Read-only observation. A failed probe never grants authority to replace a seat. */
|
|
7
|
+
export async function taskLiveReadiness(task, room, cowork, deps = {}) {
|
|
8
|
+
const degraded = (reason, member) => ({ state: 'degraded', reason, member });
|
|
9
|
+
const unknown = (reason, member) => ({ state: 'unknown', reason, member });
|
|
10
|
+
if (!room || task.state !== 'active' || task.room_id !== room.room_id || room.state !== 'active' || room.close || task.terminal_intent
|
|
11
|
+
|| room.task_id !== task.task_id || !room.room_identity_cid
|
|
12
|
+
|| task.room_identity_cid !== room.room_identity_cid)
|
|
13
|
+
return degraded('room_record_mismatch');
|
|
14
|
+
const expected = room.template_snapshot?.members.reduce((n, member) => n + member.count, 0);
|
|
15
|
+
if (expected === undefined || room.member_seats.length !== expected
|
|
16
|
+
|| new Set(room.member_seats.map(seat => seat.role_name)).size !== expected)
|
|
17
|
+
return degraded('member_roster_mismatch');
|
|
18
|
+
let remote;
|
|
19
|
+
try {
|
|
20
|
+
remote = await cowork.getRoom(room.room_id);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return unknown('room_probe_unavailable');
|
|
24
|
+
}
|
|
25
|
+
if (!remote || remote.room_id !== room.room_id || remote.identity_cid !== room.room_identity_cid
|
|
26
|
+
|| remote.state !== 'active')
|
|
27
|
+
return degraded('room_identity_or_state_mismatch');
|
|
28
|
+
if (room.owner_seat_cid && !remote.seats.some(seat => seat.identity_cid === room.owner_seat_cid && seat.seat_state === 'active'))
|
|
29
|
+
return degraded('owner_seat_missing');
|
|
30
|
+
for (const seat of room.member_seats) {
|
|
31
|
+
const member = seat.role_name;
|
|
32
|
+
if (seat.seat_state !== 'active' || !seat.identity_cid || !seat.invite_id
|
|
33
|
+
|| seat.launch?.state !== 'launched' || !seat.launch.launch_id || seat.retirement)
|
|
34
|
+
return degraded('member_record_incomplete', member);
|
|
35
|
+
const matches = remote.seats.filter(item => item.display_name === member && item.seat_state !== 'removed');
|
|
36
|
+
if (matches.length !== 1 || matches[0].identity_cid !== seat.identity_cid
|
|
37
|
+
|| matches[0].invite_id !== seat.invite_id || matches[0].role !== seat.cowork_role
|
|
38
|
+
|| matches[0].seat_state !== 'active')
|
|
39
|
+
return degraded('member_seat_mismatch', member);
|
|
40
|
+
const dir = agentDir(member, true);
|
|
41
|
+
try {
|
|
42
|
+
const supervisor = (deps.supervisor ?? readTempSupervisor)(dir);
|
|
43
|
+
if (!supervisor)
|
|
44
|
+
return unknown('supervisor_record_missing', member);
|
|
45
|
+
if (supervisor.role !== member || supervisor.launchId !== seat.launch.launch_id)
|
|
46
|
+
return degraded('supervisor_launch_mismatch', member);
|
|
47
|
+
const live = await (deps.liveness ?? tempSupervisorLiveness)(dir, {
|
|
48
|
+
exec: (cmd, args, opts) => realExec(cmd, args, { ...opts, timeout: 2_000 }),
|
|
49
|
+
});
|
|
50
|
+
if (live === 'stopped')
|
|
51
|
+
return degraded('supervisor_stopped', member);
|
|
52
|
+
if (live !== 'running')
|
|
53
|
+
return unknown('supervisor_liveness_unknown', member);
|
|
54
|
+
const ready = (deps.readiness ?? readRoomReadiness)(room.room_identity_cid, member);
|
|
55
|
+
if (!ready || ready.room !== room.room_id || ready.invite !== seat.invite_id || ready.cid !== seat.identity_cid)
|
|
56
|
+
return degraded('member_readiness_mismatch', member);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return unknown('supervisor_probe_unavailable', member);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const response = await (deps.control ?? controlRequest)(dir, { command: 'status' }, 2_000);
|
|
63
|
+
if (!response.ok)
|
|
64
|
+
return unknown('control_unavailable', member);
|
|
65
|
+
const status = response.result;
|
|
66
|
+
if (status?.alive === false || status?.readiness === 'failed')
|
|
67
|
+
return degraded('agent_not_ready', member);
|
|
68
|
+
if (status?.alive !== true || !['idle', 'running', 'awaiting_permission'].includes(status.readiness ?? ''))
|
|
69
|
+
return unknown('agent_readiness_unknown', member);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return unknown('control_unavailable', member);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "1.2.0-nightly.
|
|
3
|
+
"version": "1.2.0-nightly.12",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|