@ours.network/fleet 1.1.0-nightly.25 → 1.1.0-nightly.26
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 +20 -2
- package/dist/application/task-room-service.d.ts +30 -1
- package/dist/application/task-room-service.js +167 -104
- package/dist/build-info.json +4 -4
- package/dist/cli.js +9 -1
- package/dist/docs.d.ts +1 -1
- package/dist/docs.js +20 -2
- package/dist/fleet-command-audit.d.ts +11 -0
- package/dist/fleet-command-audit.js +47 -3
- package/dist/owner-channel/channel.d.ts +4 -0
- package/dist/owner-channel/channel.js +90 -8
- package/dist/owner-channel/commands.d.ts +7 -2
- package/dist/owner-channel/commands.js +36 -2
- package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
- package/dist/owner-channel/lifecycle-outbox.js +87 -0
- package/dist/rooms-tasks/cli.js +139 -106
- package/dist/rooms-tasks/external-worker.d.ts +8 -0
- package/dist/rooms-tasks/external-worker.js +38 -0
- package/dist/rooms-tasks/provision.js +14 -25
- package/dist/runner.js +2 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/control.js +14 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -604,8 +604,26 @@ Fleet launches each template member with a dedicated one-time Cowork invite.
|
|
|
604
604
|
The generated temporary-agent briefing contains the exact identity name, invite,
|
|
605
605
|
Cowork role, and task. The agent creates that identity itself with ours MCP
|
|
606
606
|
`create_temporary_identity`, accepts the invite with `add_contact`, and starts
|
|
607
|
-
work immediately.
|
|
608
|
-
is
|
|
607
|
+
work immediately. A room is ready only after the Cowork room (and its task, when
|
|
608
|
+
task-bound) is durably active, every configured member seat is authenticated and active with its matching
|
|
609
|
+
live Fleet launch, and the configured Owner seat is active when owner attachment is
|
|
610
|
+
enabled. There is no briefing hash, startup ACK, or separate role-briefing readiness
|
|
611
|
+
gate.
|
|
612
|
+
|
|
613
|
+
Normal provisioning emits exactly two authenticated Owner lifecycle notices: a
|
|
614
|
+
concise created notice immediately after the Cowork room is visible, and
|
|
615
|
+
`The room <name> is ready.` after the ready predicate above is true. Intermediate
|
|
616
|
+
task, saga, member-spawn, timeout, and recoverable-failure transitions stay in local
|
|
617
|
+
state and logs. A terminal failed task emits one actionable failure notice; the
|
|
618
|
+
command result carries the exact blocker and canonical recovery action.
|
|
619
|
+
|
|
620
|
+
`task start` and create-and-start wait for readiness. If their bounded wait expires,
|
|
621
|
+
they return an explicit `in_progress` result with the stable
|
|
622
|
+
`ours-fleet task await <id>` handle and start a safe continuation. `task await`
|
|
623
|
+
waits on that same durable operation and returns `ready`, `in_progress`, or
|
|
624
|
+
`failed` in both human and `--json` forms; timeout is not failure. The detached
|
|
625
|
+
continuation is serialized per task and remains alive until convergence or an
|
|
626
|
+
Owner-action blocker; invoking `task await` safely re-arms it after a restart.
|
|
609
627
|
|
|
610
628
|
Set `room.anonymous: true` on a room template, or pass `--anonymous` to
|
|
611
629
|
`task create`, `task start`, `task work`, or `room create`, to create an
|
|
@@ -37,6 +37,29 @@ export interface TaskRecoveryResult {
|
|
|
37
37
|
issues: TaskRecoveryIssue[];
|
|
38
38
|
reason?: 'missing_room' | 'missing_durable_template' | 'non_resumable_phase';
|
|
39
39
|
}
|
|
40
|
+
export interface TaskProvisioningOutcome {
|
|
41
|
+
kind: 'ready' | 'in_progress' | 'failed';
|
|
42
|
+
task: TaskRecord;
|
|
43
|
+
room?: RoomOrchestrationRecord;
|
|
44
|
+
handle: {
|
|
45
|
+
command: string;
|
|
46
|
+
task_id: string;
|
|
47
|
+
};
|
|
48
|
+
launch: {
|
|
49
|
+
template?: string;
|
|
50
|
+
anonymous: boolean;
|
|
51
|
+
owner_attached: boolean;
|
|
52
|
+
};
|
|
53
|
+
members: {
|
|
54
|
+
expected: number;
|
|
55
|
+
active: number;
|
|
56
|
+
launched: number;
|
|
57
|
+
};
|
|
58
|
+
blocker?: string;
|
|
59
|
+
next_action?: string;
|
|
60
|
+
}
|
|
61
|
+
/** Repairable terminal observation derived entirely from durable Task/Room state. */
|
|
62
|
+
export declare function recordTaskProvisioningOutcome(outcome: TaskProvisioningOutcome): void;
|
|
40
63
|
export type TaskRecoveryBegin = {
|
|
41
64
|
kind: 'terminal_worker_required';
|
|
42
65
|
taskId: string;
|
|
@@ -143,6 +166,12 @@ export declare class TaskRoomApplicationService {
|
|
|
143
166
|
task: TaskRecord;
|
|
144
167
|
orchestration: RoomOrchestrationRecord | undefined;
|
|
145
168
|
};
|
|
169
|
+
taskProvisioningOutcome(taskId: string): TaskProvisioningOutcome;
|
|
170
|
+
awaitTaskProvisioning(input: {
|
|
171
|
+
actor: TaskRoomActor;
|
|
172
|
+
taskId: string;
|
|
173
|
+
waitMs?: number;
|
|
174
|
+
}): Promise<TaskProvisioningOutcome>;
|
|
146
175
|
blockTask(input: {
|
|
147
176
|
actor: TaskRoomActor;
|
|
148
177
|
taskId: string;
|
|
@@ -306,7 +335,7 @@ export declare class TaskRoomApplicationService {
|
|
|
306
335
|
anonymous?: boolean;
|
|
307
336
|
}): Promise<{
|
|
308
337
|
task: TaskRecord;
|
|
309
|
-
status: 'ready' | 'already_active';
|
|
338
|
+
status: 'ready' | 'already_active' | 'in_progress';
|
|
310
339
|
}>;
|
|
311
340
|
private createTemplate;
|
|
312
341
|
private existingTemplate;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
2
|
import { ConfigError, loadConfig } from '../config.js';
|
|
4
3
|
import { CoworkProtocolError, CoworkUnavailableError, createCoworkAdapter, } from '../rooms-tasks/cowork-adapter.js';
|
|
5
4
|
import { getBinPath, provisionMembers } from '../rooms-tasks/provision.js';
|
|
@@ -9,7 +8,7 @@ import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked,
|
|
|
9
8
|
import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
|
|
10
9
|
import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
|
|
11
10
|
import { hashMemberOverrides, prepareExecutionPlan } from '../rooms-tasks/member-overrides.js';
|
|
12
|
-
import { recordFleetAuditPresentation } from '../fleet-command-audit.js';
|
|
11
|
+
import { checkpointFleetAuditPresentations, recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
|
|
13
12
|
import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
|
|
14
13
|
import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath, } from '../rooms-tasks/terminal.js';
|
|
15
14
|
import { acceptTaskDeletion, recordTaskDeletionError, settleTaskDeletion, } from '../rooms-tasks/deletion.js';
|
|
@@ -17,6 +16,44 @@ import { withFileLock } from '../atomic-file.js';
|
|
|
17
16
|
import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
|
|
18
17
|
import { storedRoomLaunchPolicy, TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
|
|
19
18
|
import { deriveTaskRoomName } from '../rooms-tasks/task-room-name.js';
|
|
19
|
+
function recordCanonicalRoomCreated(room) {
|
|
20
|
+
recordFleetAuditPresentation({ kind: 'room', operation: 'create',
|
|
21
|
+
eventId: `room-created:${room.created_at}`, id: room.room_id,
|
|
22
|
+
name: room.room_name, previousState: 'none', newState: 'provisioning',
|
|
23
|
+
revision: room.created_at, taskId: room.task_id,
|
|
24
|
+
template: room.template_snapshot
|
|
25
|
+
? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
26
|
+
anonymous: storedRoomLaunchPolicy(room.room_policy).anonymous,
|
|
27
|
+
memberCount: room.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0) ?? 0,
|
|
28
|
+
participants: [] });
|
|
29
|
+
}
|
|
30
|
+
/** Repairable terminal observation derived entirely from durable Task/Room state. */
|
|
31
|
+
export function recordTaskProvisioningOutcome(outcome) {
|
|
32
|
+
const room = outcome.room;
|
|
33
|
+
recordFleetAuditResource('task', outcome.task.task_id);
|
|
34
|
+
if (room) {
|
|
35
|
+
recordFleetAuditResource('room', room.room_id);
|
|
36
|
+
// Always restate created before a later terminal transition. The stable
|
|
37
|
+
// digest makes this a no-op when the original checkpoint was delivered,
|
|
38
|
+
// and repairs a crash after Room persistence but before that checkpoint.
|
|
39
|
+
recordCanonicalRoomCreated(room);
|
|
40
|
+
}
|
|
41
|
+
if (outcome.kind === 'ready' && room)
|
|
42
|
+
recordFleetAuditPresentation({
|
|
43
|
+
kind: 'room', operation: 'activate', eventId: `room-ready:${room.activated_at ?? room.created_at}`,
|
|
44
|
+
id: room.room_id, name: room.room_name, previousState: 'provisioning', newState: 'active',
|
|
45
|
+
revision: room.activated_at ?? room.created_at, taskId: room.task_id,
|
|
46
|
+
template: outcome.launch.template, anonymous: outcome.launch.anonymous,
|
|
47
|
+
memberCount: outcome.members.expected, ownerAttached: outcome.launch.owner_attached,
|
|
48
|
+
participants: [],
|
|
49
|
+
});
|
|
50
|
+
if (outcome.kind === 'failed')
|
|
51
|
+
recordFleetAuditPresentation({
|
|
52
|
+
kind: 'lifecycle_failure', resource: 'Task', id: outcome.task.task_id,
|
|
53
|
+
state: outcome.task.state, category: 'provision_failed',
|
|
54
|
+
eventId: outcome.task.ended_at ?? room?.created_at ?? outcome.task.created_at,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
20
57
|
export class TaskRoomApplicationError extends Error {
|
|
21
58
|
code;
|
|
22
59
|
fields;
|
|
@@ -31,49 +68,6 @@ export function resolveRoomLaunchPolicy(template, override) {
|
|
|
31
68
|
const anonymous = override ?? template?.room?.anonymous ?? false;
|
|
32
69
|
return { anonymous };
|
|
33
70
|
}
|
|
34
|
-
function launchSelection(definition, key) {
|
|
35
|
-
const selected = definition?.[key];
|
|
36
|
-
if (!selected || typeof selected !== 'object' || Array.isArray(selected))
|
|
37
|
-
return undefined;
|
|
38
|
-
const value = selected;
|
|
39
|
-
if (value.kind === 'ref' && typeof value.id === 'string')
|
|
40
|
-
return `ref:${value.id} (reference)`;
|
|
41
|
-
if (value.kind === 'inline' && typeof value.fingerprint === 'string')
|
|
42
|
-
return `inline:${value.fingerprint} (inline)`;
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
function launchPermissions(definition) {
|
|
46
|
-
const selected = definition?.permissions;
|
|
47
|
-
if (!selected || typeof selected !== 'object' || Array.isArray(selected))
|
|
48
|
-
return undefined;
|
|
49
|
-
const value = selected;
|
|
50
|
-
const fields = ['approval', 'filesystem', 'unattended'].flatMap(key => typeof value[key] === 'string' ? [`${key}=${value[key]}`] : []);
|
|
51
|
-
return fields.length ? fields.join(',') : undefined;
|
|
52
|
-
}
|
|
53
|
-
function roomParticipants(room) {
|
|
54
|
-
return [...room.member_seats].sort((a, b) => a.role_name.localeCompare(b.role_name)).map(seat => ({
|
|
55
|
-
name: seat.role_name, id: seat.identity_cid,
|
|
56
|
-
brain: launchSelection(seat.launch?.agent_definition, 'brain'),
|
|
57
|
-
role: launchSelection(seat.launch?.agent_definition, 'role') ?? seat.cowork_role,
|
|
58
|
-
permissions: launchPermissions(seat.launch?.agent_definition),
|
|
59
|
-
...(seat.launch?.presentation ? { configuration: seat.launch.presentation } : {}),
|
|
60
|
-
}));
|
|
61
|
-
}
|
|
62
|
-
function taskAgents(task) {
|
|
63
|
-
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
64
|
-
const seats = new Map(room?.member_seats.map(seat => [seat.role_name, seat]) ?? []);
|
|
65
|
-
return [...task.member_roles].sort((a, b) => a.name.localeCompare(b.name)).map(member => {
|
|
66
|
-
const seat = seats.get(member.name);
|
|
67
|
-
return { name: member.name, brain: launchSelection(seat?.launch?.agent_definition, 'brain'),
|
|
68
|
-
role: launchSelection(seat?.launch?.agent_definition, 'role') ?? member.cowork_role,
|
|
69
|
-
permissions: launchPermissions(seat?.launch?.agent_definition),
|
|
70
|
-
...(seat?.launch?.presentation ? { configuration: seat.launch.presentation } : {}) };
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
function roomFailureEventId(room) {
|
|
74
|
-
const evidence = `${room.created_at}\0${room.saga.phase}\0${room.saga.step_index}\0${room.saga.error ?? ''}`;
|
|
75
|
-
return `sha256:${createHash('sha256').update(evidence).digest('hex')}`;
|
|
76
|
-
}
|
|
77
71
|
/** Exact extraction of the previously CLI-owned task create/start behavior. */
|
|
78
72
|
export class TaskRoomApplicationService {
|
|
79
73
|
configurationPath;
|
|
@@ -129,35 +123,33 @@ export class TaskRoomApplicationService {
|
|
|
129
123
|
}
|
|
130
124
|
unlock?.();
|
|
131
125
|
const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
126
|
+
// A create-and-start launch is represented to the Owner solely by the
|
|
127
|
+
// canonical Room created/ready events. Backlog and no-room tasks retain
|
|
128
|
+
// their useful Task lifecycle notice.
|
|
129
|
+
if (request.backlog || request.noRoom)
|
|
130
|
+
recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
|
|
131
|
+
title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
|
|
132
|
+
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
133
|
+
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
134
|
+
agents: [] });
|
|
137
135
|
if (!request.backlog && !request.noRoom && ref && !task.room_id) {
|
|
138
136
|
try {
|
|
139
137
|
const room = await this.provisionRoom(cfg, task, template, created => {
|
|
140
138
|
task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
|
|
141
139
|
}, launchDefinitions, roomPolicy);
|
|
142
140
|
task = readTask(task.task_id);
|
|
143
|
-
|
|
144
|
-
recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
|
|
145
|
-
title: task.title, previousState: 'provisioning', newState: task.state,
|
|
146
|
-
revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
|
|
147
|
-
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
148
|
-
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
149
|
-
agents: taskAgents(task) });
|
|
150
|
-
else
|
|
151
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
|
|
152
|
-
state: task.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
|
|
141
|
+
void room;
|
|
153
142
|
}
|
|
154
143
|
catch (error) {
|
|
155
144
|
if (error instanceof CoworkUnavailableError)
|
|
156
145
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
146
|
+
// Once the durable Room exists, provisioning errors are resumable
|
|
147
|
+
// saga state. Return that explicit state so the command can launch a
|
|
148
|
+
// continuation and give the caller a stable await handle.
|
|
149
|
+
const current = readTask(task.task_id);
|
|
150
|
+
if (!current.room_id || !getRoomRecord(current.room_id))
|
|
151
|
+
throw error;
|
|
152
|
+
task = current;
|
|
161
153
|
}
|
|
162
154
|
}
|
|
163
155
|
return task;
|
|
@@ -232,6 +224,54 @@ export class TaskRoomApplicationService {
|
|
|
232
224
|
const task = readTask(taskId);
|
|
233
225
|
return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
|
|
234
226
|
}
|
|
227
|
+
taskProvisioningOutcome(taskId) {
|
|
228
|
+
const task = readTask(taskId);
|
|
229
|
+
const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
|
|
230
|
+
const expected = room?.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0)
|
|
231
|
+
?? task.execution_plan?.snapshot.members.reduce((sum, member) => sum + member.count, 0) ?? 0;
|
|
232
|
+
const active = room?.member_seats.filter(seat => seat.seat_state === 'active').length ?? 0;
|
|
233
|
+
const launched = room?.member_seats.filter(seat => seat.launch?.state === 'launched').length ?? 0;
|
|
234
|
+
const failed = task.state === 'failed' || room?.saga.phase === 'failed';
|
|
235
|
+
const ready = task.state === 'active' && room?.state === 'active'
|
|
236
|
+
&& active === expected && launched === expected;
|
|
237
|
+
const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
|
|
238
|
+
const nextAction = room?.provisioning_detail === 'waiting_owner_invite'
|
|
239
|
+
? 'Rotate rooms.owner.public_invite, then await the same task.'
|
|
240
|
+
: room?.provisioning_detail === 'owner_cid_mismatch'
|
|
241
|
+
? 'Verify rooms.owner.expected_cid, rotate the Owner invite, then await the same task.'
|
|
242
|
+
: room?.provisioning_detail === 'waiting_cowork'
|
|
243
|
+
? 'Restore ours-cowork, then await the same task.'
|
|
244
|
+
: failed
|
|
245
|
+
? `Correct the blocker, then run ours-fleet task recover ${task.task_id}.`
|
|
246
|
+
: undefined;
|
|
247
|
+
return {
|
|
248
|
+
kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
|
|
249
|
+
handle: { command: `ours-fleet task await ${task.task_id}`, task_id: task.task_id },
|
|
250
|
+
launch: {
|
|
251
|
+
...(room?.template_snapshot
|
|
252
|
+
? { template: `${room.template_snapshot.name}@${room.template_snapshot.version}` }
|
|
253
|
+
: task.template ? { template: `${task.template.name}@${task.template.version}` } : {}),
|
|
254
|
+
anonymous: storedRoomLaunchPolicy(room?.room_policy ?? task.execution_plan?.room_policy).anonymous,
|
|
255
|
+
owner_attached: Boolean(room?.owner_seat_cid),
|
|
256
|
+
},
|
|
257
|
+
members: { expected, active, launched },
|
|
258
|
+
...(blocker ? { blocker } : {}),
|
|
259
|
+
...(nextAction ? { next_action: nextAction } : {}),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async awaitTaskProvisioning(input) {
|
|
263
|
+
const now = this.deps.now ?? Date.now;
|
|
264
|
+
const sleep = this.deps.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
265
|
+
const deadline = now() + (input.waitMs ?? 60_000);
|
|
266
|
+
for (;;) {
|
|
267
|
+
const outcome = this.taskProvisioningOutcome(input.taskId);
|
|
268
|
+
if (outcome.kind !== 'in_progress' || now() >= deadline) {
|
|
269
|
+
recordTaskProvisioningOutcome(outcome);
|
|
270
|
+
return outcome;
|
|
271
|
+
}
|
|
272
|
+
await sleep(100);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
235
275
|
blockTask(input) {
|
|
236
276
|
return persistBlockTask(input.taskId, input.reason);
|
|
237
277
|
}
|
|
@@ -422,6 +462,26 @@ export class TaskRoomApplicationService {
|
|
|
422
462
|
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
423
463
|
}
|
|
424
464
|
}
|
|
465
|
+
if (room.provisioning_detail === 'waiting_owner_invite'
|
|
466
|
+
|| room.provisioning_detail === 'owner_cid_mismatch') {
|
|
467
|
+
try {
|
|
468
|
+
await this.recoverRoom({ actor: input.actor, roomId: room.room_id });
|
|
469
|
+
task = readTask(task.task_id);
|
|
470
|
+
room = getRoomRecord(room.room_id);
|
|
471
|
+
if (task.state === 'active' && room?.state === 'active')
|
|
472
|
+
return {
|
|
473
|
+
kind: 'provisioning_resumed', task, room, issues: [{ code: 'provisioning_resumed' }],
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
catch (error) {
|
|
477
|
+
issues.push({ code: 'resume_failed', error: error instanceof Error ? error.message : String(error) });
|
|
478
|
+
return { kind: 'provisioning_resume_failed', task, room, issues };
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (!room)
|
|
482
|
+
return {
|
|
483
|
+
kind: 'provisioning_non_resumable', task, room, issues, reason: 'missing_room',
|
|
484
|
+
};
|
|
425
485
|
if (room.provisioning_detail === 'waiting_cowork')
|
|
426
486
|
issues.push({ code: 'waiting_cowork' });
|
|
427
487
|
if (room.provisioning_detail === 'waiting_owner_invite')
|
|
@@ -625,6 +685,16 @@ export class TaskRoomApplicationService {
|
|
|
625
685
|
return { kind: 'provisioning_resume_failed', room, orchestration, issues };
|
|
626
686
|
}
|
|
627
687
|
}
|
|
688
|
+
if (orchestration?.state === 'provisioning' && !orchestration.template_snapshot) {
|
|
689
|
+
const seats = await adapter.getSeats(input.roomId);
|
|
690
|
+
const ownerReady = !orchestration.owner_seat_cid || seats.some(seat => seat.identity_cid.toLowerCase() === orchestration.owner_seat_cid.toLowerCase()
|
|
691
|
+
&& seat.seat_state === 'active');
|
|
692
|
+
if (ownerReady) {
|
|
693
|
+
orchestration = activateRoom(input.roomId);
|
|
694
|
+
if (orchestration.task_id)
|
|
695
|
+
activateTask(orchestration.task_id);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
628
698
|
return { kind: 'recovered', room, orchestration, issues };
|
|
629
699
|
}
|
|
630
700
|
async finishTask(input) {
|
|
@@ -762,10 +832,6 @@ export class TaskRoomApplicationService {
|
|
|
762
832
|
task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
|
|
763
833
|
if (task.state === 'backlog') {
|
|
764
834
|
task = transitionTask(task.task_id);
|
|
765
|
-
recordFleetAuditPresentation({ kind: 'task', operation: 'start', id: task.task_id,
|
|
766
|
-
title: task.title, previousState: 'backlog', newState: task.state,
|
|
767
|
-
revision: task.started_at ?? task.created_at, list: task.list_name ?? 'default',
|
|
768
|
-
template: `${snapshot.name}@${snapshot.version}`, agents: [] });
|
|
769
835
|
}
|
|
770
836
|
if (!task.room_id) {
|
|
771
837
|
try {
|
|
@@ -777,10 +843,10 @@ export class TaskRoomApplicationService {
|
|
|
777
843
|
catch (error) {
|
|
778
844
|
if (error instanceof CoworkUnavailableError)
|
|
779
845
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
846
|
+
const current = readTask(task.task_id);
|
|
847
|
+
if (!current.room_id || !getRoomRecord(current.room_id))
|
|
848
|
+
throw error;
|
|
849
|
+
task = current;
|
|
784
850
|
}
|
|
785
851
|
}
|
|
786
852
|
else if (task.state === 'provisioning') {
|
|
@@ -798,24 +864,11 @@ export class TaskRoomApplicationService {
|
|
|
798
864
|
catch (error) {
|
|
799
865
|
if (error instanceof CoworkUnavailableError)
|
|
800
866
|
persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
|
|
801
|
-
|
|
802
|
-
id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
|
|
803
|
-
eventId: roomFailureEventId(getRoomRecord(task.room_id)) });
|
|
804
|
-
throw error;
|
|
867
|
+
task = readTask(task.task_id);
|
|
805
868
|
}
|
|
806
869
|
}
|
|
807
870
|
const resultingRoom = getRoomRecord(task.room_id);
|
|
808
|
-
|
|
809
|
-
recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
|
|
810
|
-
title: task.title, previousState: 'provisioning', newState: task.state,
|
|
811
|
-
revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
|
|
812
|
-
list: task.list_name ?? 'default', roomId: task.room_id,
|
|
813
|
-
template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
|
|
814
|
-
agents: taskAgents(task) });
|
|
815
|
-
else if (resultingRoom)
|
|
816
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
|
|
817
|
-
id: task.task_id, state: task.state, category: 'provision_pending', eventId: roomFailureEventId(resultingRoom) });
|
|
818
|
-
return { task, status: 'ready' };
|
|
871
|
+
return { task, status: task.state === 'active' && resultingRoom?.state === 'active' ? 'ready' : 'in_progress' };
|
|
819
872
|
}
|
|
820
873
|
createTemplate(cfg, requested, noRoom) {
|
|
821
874
|
if (noRoom)
|
|
@@ -892,11 +945,10 @@ export class TaskRoomApplicationService {
|
|
|
892
945
|
throw error;
|
|
893
946
|
}
|
|
894
947
|
unlockSnapshot?.();
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
participants: [] });
|
|
948
|
+
recordCanonicalRoomCreated(room);
|
|
949
|
+
// The created notice is observable while member admission is still in
|
|
950
|
+
// progress; it is not delayed behind task-start command completion.
|
|
951
|
+
await checkpointFleetAuditPresentations();
|
|
900
952
|
room = advanceSaga(room.room_id, 'create_room', 1);
|
|
901
953
|
if (attachOwner) {
|
|
902
954
|
try {
|
|
@@ -910,14 +962,11 @@ export class TaskRoomApplicationService {
|
|
|
910
962
|
const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
|
|
911
963
|
setSagaError(room.room_id, error instanceof Error ? error.message : String(error), mismatch ? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
|
|
912
964
|
: 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
|
|
913
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
|
|
914
|
-
id: room.room_id, state: getRoomRecord(room.room_id)?.state ?? 'provisioning',
|
|
915
|
-
category: 'provision_failed', eventId: roomFailureEventId(getRoomRecord(room.room_id) ?? room) });
|
|
916
965
|
throw error;
|
|
917
966
|
}
|
|
918
967
|
}
|
|
919
968
|
room = advanceSaga(room.room_id, 'create_members', 3);
|
|
920
|
-
if (launchTemplate
|
|
969
|
+
if (launchTemplate && (launchTemplate.members.length > 0 || attachOwner))
|
|
921
970
|
try {
|
|
922
971
|
room = await (this.deps.provisionMembers ?? provisionMembers)({
|
|
923
972
|
cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
|
|
@@ -926,22 +975,36 @@ export class TaskRoomApplicationService {
|
|
|
926
975
|
});
|
|
927
976
|
}
|
|
928
977
|
catch (error) {
|
|
929
|
-
const failed = getRoomRecord(room.room_id) ?? room;
|
|
930
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: failed.room_id,
|
|
931
|
-
state: failed.state, category: 'provision_failed', eventId: roomFailureEventId(failed) });
|
|
932
978
|
throw error;
|
|
933
979
|
}
|
|
980
|
+
else if (attachOwner) {
|
|
981
|
+
const now = this.deps.now ?? Date.now;
|
|
982
|
+
const sleep = this.deps.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
|
|
983
|
+
const deadline = now() + 60_000;
|
|
984
|
+
for (;;) {
|
|
985
|
+
const seats = await cowork.getSeats(room.room_id);
|
|
986
|
+
if (room.owner_seat_cid && seats.some(seat => seat.identity_cid.toLowerCase() === room.owner_seat_cid.toLowerCase()
|
|
987
|
+
&& seat.seat_state === 'active')) {
|
|
988
|
+
room = activateRoom(room.room_id);
|
|
989
|
+
break;
|
|
990
|
+
}
|
|
991
|
+
if (now() >= deadline) {
|
|
992
|
+
room = advanceSaga(room.room_id, 'wait_seats', 5, 'waiting_seats');
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
await sleep(250);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
934
998
|
else
|
|
935
999
|
room = activateRoom(room.room_id);
|
|
936
1000
|
if (room.state === 'active')
|
|
937
|
-
recordFleetAuditPresentation({ kind: 'room', operation: 'activate',
|
|
1001
|
+
recordFleetAuditPresentation({ kind: 'room', operation: 'activate',
|
|
1002
|
+
eventId: `room-ready:${room.activated_at ?? room.created_at}`, id: room.room_id,
|
|
938
1003
|
name: room.room_name, previousState: 'provisioning', newState: 'active',
|
|
939
1004
|
revision: room.activated_at ?? room.created_at, taskId: room.task_id,
|
|
940
1005
|
template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: room.room_id,
|
|
944
|
-
state: room.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
|
|
1006
|
+
anonymous: policy.anonymous, memberCount: room.member_seats.length, ownerAttached: attachOwner,
|
|
1007
|
+
participants: [] });
|
|
945
1008
|
if (task.task_id && !launchTemplate?.members.length)
|
|
946
1009
|
activateTask(task.task_id);
|
|
947
1010
|
return room;
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.1.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.1.0-nightly.26",
|
|
3
|
+
"buildId": "ecccd36d964c",
|
|
4
|
+
"commit": "d8242c7a62cd89e52b082241f0e26946a14b0d14",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-03T11:39:31.486Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"monitor.interrupt.after_tool"
|
|
9
9
|
]
|
package/dist/cli.js
CHANGED
|
@@ -40,7 +40,7 @@ import { requestWebControl } from './web/control.js';
|
|
|
40
40
|
import { WebServiceManager } from './web/service.js';
|
|
41
41
|
import { WebAccessStore, passwordAccess, validatePublicOrigin } from './web/access.js';
|
|
42
42
|
import { FLEET_PROXY_CALLER_ENV, FLEET_PROXY_STATE_DIR_ENV, } from './fleet-proxy.js';
|
|
43
|
-
import { beginFleetAuditCollection, consumeFleetAuditCollection, FleetCliExit, } from './fleet-command-audit.js';
|
|
43
|
+
import { beginFleetAuditCollection, consumeFleetAuditCollection, FleetCliExit, setFleetAuditLifecycleCheckpoint, } from './fleet-command-audit.js';
|
|
44
44
|
import './harness/claude-code.js'; // registers the claude-code adapter
|
|
45
45
|
import './harness/codex.js'; // registers the codex adapter
|
|
46
46
|
import { registerTemplateCommands, registerTaskCommands, registerRoomCommands } from './rooms-tasks/cli.js';
|
|
@@ -1621,6 +1621,13 @@ async function runFleetCli() {
|
|
|
1621
1621
|
let outcomeClass = 'success';
|
|
1622
1622
|
let effect = 'completed';
|
|
1623
1623
|
beginFleetAuditCollection();
|
|
1624
|
+
setFleetAuditLifecycleCheckpoint(async (presentations) => {
|
|
1625
|
+
const response = await controlRequest(stateDir, {
|
|
1626
|
+
command: 'fleet_audit_present', audit: { presentations },
|
|
1627
|
+
});
|
|
1628
|
+
if (!response.ok)
|
|
1629
|
+
throw new SessionControlError(response.kind ?? 'backend', response.error ?? 'Fleet lifecycle checkpoint delivery failed');
|
|
1630
|
+
});
|
|
1624
1631
|
if (attempt.classification.decision !== 'allow') {
|
|
1625
1632
|
exitCode = 1;
|
|
1626
1633
|
outcomeClass = 'denied';
|
|
@@ -1644,6 +1651,7 @@ async function runFleetCli() {
|
|
|
1644
1651
|
process.exit = originalExit;
|
|
1645
1652
|
}
|
|
1646
1653
|
}
|
|
1654
|
+
setFleetAuditLifecycleCheckpoint(undefined);
|
|
1647
1655
|
const metadata = consumeFleetAuditCollection();
|
|
1648
1656
|
if (metadata.failure) {
|
|
1649
1657
|
outcomeClass = metadata.failure.class;
|