@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 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. Fleet activates the room from Cowork's authenticated seat; there
608
- is no briefing hash, startup ACK, or separate role-briefing readiness gate.
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
- recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
133
- title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
134
- list: task.list_name ?? 'default', roomId: task.room_id,
135
- template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
136
- agents: [] });
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
- if (task.state === 'active' && room.state === 'active')
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
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
158
- id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
159
- eventId: task.room_id ? roomFailureEventId(getRoomRecord(task.room_id)) : task.created_at });
160
- throw error;
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
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
781
- id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
782
- eventId: task.room_id ? roomFailureEventId(getRoomRecord(task.room_id)) : task.created_at });
783
- throw error;
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
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
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
- if (task.state === 'active' && resultingRoom?.state === 'active')
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
- recordFleetAuditPresentation({ kind: 'room', operation: 'create', id: room.room_id,
896
- name: room.room_name, previousState: 'none', newState: 'provisioning',
897
- revision: room.created_at, taskId: room.task_id,
898
- template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
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?.members.length)
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', id: room.room_id,
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
- participants: roomParticipants(room) });
942
- else
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;
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.1.0-nightly.25",
3
- "buildId": "648d41abce4a",
4
- "commit": "d258b3f616a842c4f4ff00b755880d080d885db3",
2
+ "version": "1.1.0-nightly.26",
3
+ "buildId": "ecccd36d964c",
4
+ "commit": "d8242c7a62cd89e52b082241f0e26946a14b0d14",
5
5
  "dirty": true,
6
- "builtAt": "2026-09-02T15:13:51.477Z",
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;