@ours.network/fleet 1.1.0-nightly.25 → 1.1.0-nightly.27

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
@@ -78,8 +78,7 @@ The state dir contract:
78
78
 
79
79
  | What | Why | Install |
80
80
  |---|---|---|
81
- | Node ≥ 20 | runs `ours-fleet` itself | nodejs.org, `apt`, or `brew` |
82
- | Node ≥ 22 | Claude roles using `session: acp` | required by the maintained Claude ACP adapter |
81
+ | Node ≥ 22 | runs `ours-fleet` and its maintained adapters | nodejs.org, `apt`, or `brew` |
83
82
  | a harness CLI, logged in | the agent itself | e.g. Claude Code (`claude`) or Codex CLI (`codex`) |
84
83
  | `ours` CLI + shared daemon | identity + agent-to-agent messaging | `npm i -g @ours.network/cli && ours daemon start` |
85
84
 
@@ -99,8 +98,8 @@ Native Codex roles use the logged-in Codex CLI's `app-server` command directly.
99
98
  The maintained Codex and Claude ACP adapters remain bundled optional dependencies
100
99
  and are resolved internally; users do not install adapter commands or add them to
101
100
  `PATH`. Explicit `session_options.codex_app_server.command` and
102
- `session_options.acp.command` overrides remain available. On Node 20–21, native
103
- Codex and Codex ACP remain available, while maintained Claude ACP requires Node 22.
101
+ `session_options.acp.command` overrides remain available. ours-fleet and its
102
+ maintained adapters require Node 22 or newer.
104
103
 
105
104
  Each OS user manages their own fleet — to host roles under a sandboxed account,
106
105
  become that account and repeat.
@@ -604,8 +603,26 @@ Fleet launches each template member with a dedicated one-time Cowork invite.
604
603
  The generated temporary-agent briefing contains the exact identity name, invite,
605
604
  Cowork role, and task. The agent creates that identity itself with ours MCP
606
605
  `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.
606
+ work immediately. A room is ready only after the Cowork room (and its task, when
607
+ task-bound) is durably active, every configured member seat is authenticated and active with its matching
608
+ live Fleet launch, and the configured Owner seat is active when owner attachment is
609
+ enabled. There is no briefing hash, startup ACK, or separate role-briefing readiness
610
+ gate.
611
+
612
+ Normal provisioning emits exactly two authenticated Owner lifecycle notices: a
613
+ concise created notice immediately after the Cowork room is visible, and
614
+ `The room <name> is ready.` after the ready predicate above is true. Intermediate
615
+ task, saga, member-spawn, timeout, and recoverable-failure transitions stay in local
616
+ state and logs. A terminal failed task emits one actionable failure notice; the
617
+ command result carries the exact blocker and canonical recovery action.
618
+
619
+ `task start` and create-and-start wait for readiness. If their bounded wait expires,
620
+ they return an explicit `in_progress` result with the stable
621
+ `ours-fleet task await <id>` handle and start a safe continuation. `task await`
622
+ waits on that same durable operation and returns `ready`, `in_progress`, or
623
+ `failed` in both human and `--json` forms; timeout is not failure. The detached
624
+ continuation is serialized per task and remains alive until convergence or an
625
+ Owner-action blocker; invoking `task await` safely re-arms it after a restart.
609
626
 
610
627
  Set `room.anonymous: true` on a room template, or pass `--anonymous` to
611
628
  `task create`, `task start`, `task work`, or `room create`, to create an
@@ -1042,6 +1059,15 @@ unchanged. The registry in `src/owner-channel/commands.ts` is the single source
1042
1059
  of truth — `/help` renders exactly that table, so adding an entry there is the
1043
1060
  whole registration step for a new command.
1044
1061
 
1062
+ The supervisor also advertises every primary registry entry through ours typed
1063
+ commands. The menu's `arguments` field is converted back to the exact text after
1064
+ the slash command name and enters the same dispatcher, so validation, replies,
1065
+ lifecycle effects, and audit behavior stay identical. Aliases remain available
1066
+ as slash commands but are not duplicated in the typed menu. Typed handlers
1067
+ re-check the authenticated sender CID against the live Owner boundary before
1068
+ dispatch; the SDK completion result is `null` because the existing correlated
1069
+ owner-channel reply remains the command result.
1070
+
1045
1071
  | Command | Effect |
1046
1072
  | --- | --- |
1047
1073
  | `/help` (alias `/commands`) | list all deterministic owner-channel commands |
@@ -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.27",
3
+ "buildId": "188ee458bb22",
4
+ "commit": "0ace1644c06e5c637efccced7b39bdfa03260cf6",
5
5
  "dirty": true,
6
- "builtAt": "2026-09-02T15:13:51.477Z",
6
+ "builtAt": "2026-09-03T18:06:33.704Z",
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;
package/dist/creation.js CHANGED
@@ -258,11 +258,13 @@ export function daemonIdentityProvisioner(env = process.env, attachClient = atta
258
258
  const before = await client.listIdentities();
259
259
  const existing = before.find(identity => identity.name === name);
260
260
  if (existing) {
261
+ if (!('kind' in existing))
262
+ throw new Error(`identity '${name}' is quarantined; refusing to replace it`);
261
263
  if (existing.temp)
262
264
  throw new Error(`identity '${name}' exists but is temporary; refusing to convert or adopt it`);
263
265
  return; // another reconciler won the create race
264
266
  }
265
- if (!before.some(identity => identity.kind === 'root'))
267
+ if (!before.some(identity => 'kind' in identity && identity.kind === 'root'))
266
268
  throw new Error(`cannot create role identity '${name}': this host has no Human identity; run ours onboarding first`);
267
269
  let createdHere = false;
268
270
  try {
@@ -279,7 +281,7 @@ export function daemonIdentityProvisioner(env = process.env, attachClient = atta
279
281
  // accept only the compatible permanent identity now visible.
280
282
  const after = await client.listIdentities();
281
283
  const raced = after.find(identity => identity.name === name);
282
- if (!raced || raced.temp)
284
+ if (!raced || !('kind' in raced) || raced.temp)
283
285
  throw error;
284
286
  }
285
287
  if (createdHere && profile.persona && client.setPersona)