@ours.network/fleet 1.1.0-nightly.28 → 1.1.0-nightly.29

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
@@ -609,20 +609,18 @@ live Fleet launch, and the configured Owner seat is active when owner attachment
609
609
  enabled. There is no briefing hash, startup ACK, or separate role-briefing readiness
610
610
  gate.
611
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.
612
+ Normal Task provisioning emits exactly one authenticated Owner lifecycle notice after
613
+ the ready predicate above is true; standalone Room provisioning likewise emits one
614
+ ready notice. Intermediate task, saga, member-spawn, timeout, and recoverable-failure
615
+ transitions stay in local state and logs. A terminal failed Task emits one actionable
616
+ failure notice; the command result carries the exact blocker and canonical recovery
617
+ action.
618
618
 
619
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.
620
+ they return an explicit `in_progress` result and start a safe continuation. The detached
621
+ continuation is serialized per Task and remains alive until convergence or an
622
+ Owner-action blocker. Re-running `task start <id>` safely resumes the same durable
623
+ provisioning operation after the blocker is corrected or a process restarts.
626
624
 
627
625
  Set `room.anonymous: true` on a room template, or pass `--anonymous` to
628
626
  `task create`, `task start`, `task work`, or `room create`, to create an
@@ -21,8 +21,8 @@ export interface TaskSettlementPlan {
21
21
  task: TaskRecord;
22
22
  settlementRequired: boolean;
23
23
  }
24
- export type TaskRecoveryIssue = {
25
- code: 'terminal_pending' | 'waiting_cowork' | 'waiting_owner_authorization' | 'waiting_owner_invite' | 'owner_cid_mismatch' | 'waiting_seats' | 'provisioning_resumed';
24
+ export type TaskProvisioningContinuationIssue = {
25
+ code: 'waiting_cowork' | 'waiting_owner_authorization' | 'waiting_owner_invite' | 'owner_cid_mismatch' | 'waiting_seats' | 'provisioning_resumed';
26
26
  } | {
27
27
  code: 'member_failed';
28
28
  stepIndex: number;
@@ -30,21 +30,17 @@ export type TaskRecoveryIssue = {
30
30
  code: 'resume_failed';
31
31
  error: string;
32
32
  };
33
- export interface TaskRecoveryResult {
34
- kind: 'provisioning_resumed' | 'provisioning_resume_failed' | 'provisioning_non_resumable' | 'terminal' | 'no_op';
33
+ export interface TaskProvisioningContinuationResult {
34
+ kind: 'provisioning_resumed' | 'provisioning_resume_failed' | 'provisioning_non_resumable' | 'no_op';
35
35
  task: TaskRecord;
36
36
  room: RoomOrchestrationRecord | undefined;
37
- issues: TaskRecoveryIssue[];
37
+ issues: TaskProvisioningContinuationIssue[];
38
38
  reason?: 'missing_room' | 'missing_durable_template' | 'non_resumable_phase';
39
39
  }
40
40
  export interface TaskProvisioningOutcome {
41
41
  kind: 'ready' | 'in_progress' | 'failed';
42
42
  task: TaskRecord;
43
43
  room?: RoomOrchestrationRecord;
44
- handle: {
45
- command: string;
46
- task_id: string;
47
- };
48
44
  launch: {
49
45
  template?: string;
50
46
  anonymous: boolean;
@@ -60,16 +56,6 @@ export interface TaskProvisioningOutcome {
60
56
  }
61
57
  /** Repairable terminal observation derived entirely from durable Task/Room state. */
62
58
  export declare function recordTaskProvisioningOutcome(outcome: TaskProvisioningOutcome): void;
63
- export type TaskRecoveryBegin = {
64
- kind: 'terminal_worker_required';
65
- taskId: string;
66
- } | {
67
- kind: 'deletion_worker_required';
68
- taskId: string;
69
- } | {
70
- kind: 'final';
71
- result: TaskRecoveryResult;
72
- };
73
59
  export declare class TaskRoomApplicationError extends Error {
74
60
  readonly code: 'template_not_found' | 'task_template_drift' | 'template_mismatch' | 'task_terminal' | 'task_terminal_already' | 'task_non_resumable' | 'task_deleting' | 'room_not_found' | 'room_record_not_found';
75
61
  readonly fields: Readonly<Record<string, string>>;
@@ -114,7 +100,6 @@ export interface TaskRoomServiceDeps {
114
100
  export declare class TaskRoomApplicationService {
115
101
  private readonly configurationPath?;
116
102
  private readonly deps;
117
- private recovery?;
118
103
  constructor(configurationPath?: string | undefined, deps?: TaskRoomServiceDeps);
119
104
  createTask(request: CreateTaskRequest): Promise<TaskRecord>;
120
105
  createRoom(request: CreateRoomRequest): Promise<RoomOrchestrationRecord>;
@@ -241,15 +226,14 @@ export declare class TaskRoomApplicationService {
241
226
  error: string;
242
227
  recoveryHint: string;
243
228
  }): Promise<TaskRecord>;
244
- beginTaskRecovery(input: {
245
- actor: TaskRoomActor;
246
- taskId: string;
247
- }): Promise<TaskRecoveryBegin>;
248
- continueTaskRecovery(input: {
249
- actor: TaskRoomActor;
229
+ continueTaskProvisioning(input: {
230
+ actor: {
231
+ kind: 'internal_worker';
232
+ surface: 'cli';
233
+ };
250
234
  taskId: string;
251
- terminalTimedOut: boolean;
252
- }): Promise<TaskRecoveryResult>;
235
+ }): Promise<TaskProvisioningContinuationResult>;
236
+ private reconcileProvisioningOwner;
253
237
  private acceptTerminal;
254
238
  listTemplates(): import("../rooms-tasks/types.js").TemplateDefinition[];
255
239
  getTemplate(name: string): {
@@ -310,18 +294,6 @@ export declare class TaskRoomApplicationService {
310
294
  error: string;
311
295
  recoveryHint: string;
312
296
  }): Promise<RoomOrchestrationRecord>;
313
- recoverRoom(input: {
314
- actor: TaskRoomActor;
315
- roomId: string;
316
- }): Promise<{
317
- kind: 'deletion_worker_required';
318
- roomId: string;
319
- } | {
320
- kind: 'recovered' | 'provisioning_resumed' | 'provisioning_resume_failed';
321
- room: Awaited<ReturnType<CoworkAdapter['recoverRoom']>>;
322
- orchestration: RoomOrchestrationRecord | undefined;
323
- issues: string[];
324
- }>;
325
297
  finishTask(input: {
326
298
  actor: TaskRoomActor;
327
299
  taskId: string;
@@ -8,7 +8,7 @@ import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked,
8
8
  import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
9
9
  import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
10
10
  import { hashMemberOverrides, prepareExecutionPlan } from '../rooms-tasks/member-overrides.js';
11
- import { checkpointFleetAuditPresentations, recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
11
+ import { recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
12
12
  import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
13
13
  import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath, } from '../rooms-tasks/terminal.js';
14
14
  import { acceptTaskDeletion, recordTaskDeletionError, settleTaskDeletion, } from '../rooms-tasks/deletion.js';
@@ -17,43 +17,36 @@ import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
17
17
  import { storedRoomLaunchPolicy, TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
18
18
  import { deriveTaskRoomName } from '../rooms-tasks/task-room-name.js';
19
19
  const OWNER_ROOM_COMMANDS = ['list-members', 'remove-member'];
20
- function recordCanonicalRoomCreated(room) {
21
- recordFleetAuditPresentation({ kind: 'room', operation: 'create',
22
- eventId: `room-created:${room.created_at}`, id: room.room_id,
23
- name: room.room_name, previousState: 'none', newState: 'provisioning',
24
- revision: room.created_at, taskId: room.task_id,
25
- template: room.template_snapshot
26
- ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
27
- anonymous: storedRoomLaunchPolicy(room.room_policy).anonymous,
28
- memberCount: room.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0) ?? 0,
29
- participants: [] });
30
- }
31
20
  /** Repairable terminal observation derived entirely from durable Task/Room state. */
32
21
  export function recordTaskProvisioningOutcome(outcome) {
33
22
  const room = outcome.room;
34
23
  recordFleetAuditResource('task', outcome.task.task_id);
35
- if (room) {
24
+ if (room)
36
25
  recordFleetAuditResource('room', room.room_id);
37
- // Always restate created before a later terminal transition. The stable
38
- // digest makes this a no-op when the original checkpoint was delivered,
39
- // and repairs a crash after Room persistence but before that checkpoint.
40
- recordCanonicalRoomCreated(room);
41
- }
42
26
  if (outcome.kind === 'ready' && room)
43
27
  recordFleetAuditPresentation({
44
- kind: 'room', operation: 'activate', eventId: `room-ready:${room.activated_at ?? room.created_at}`,
45
- id: room.room_id, name: room.room_name, previousState: 'provisioning', newState: 'active',
46
- revision: room.activated_at ?? room.created_at, taskId: room.task_id,
47
- template: outcome.launch.template, anonymous: outcome.launch.anonymous,
48
- memberCount: outcome.members.expected, ownerAttached: outcome.launch.owner_attached,
49
- participants: [],
28
+ kind: 'task', operation: 'work', eventId: `task-ready:${room.activated_at ?? room.created_at}`,
29
+ id: outcome.task.task_id, title: outcome.task.title,
30
+ previousState: 'provisioning', newState: 'active', revision: room.activated_at ?? room.created_at,
31
+ list: outcome.task.list_name ?? 'default', roomId: room.room_id, roomName: room.room_name,
32
+ template: outcome.launch.template,
33
+ agents: room.member_seats.map(member => ({ name: member.role_name, role: member.cowork_role,
34
+ configuration: member.launch?.presentation })),
50
35
  });
51
36
  if (outcome.kind === 'failed')
52
37
  recordFleetAuditPresentation({
53
38
  kind: 'lifecycle_failure', resource: 'Task', id: outcome.task.task_id,
39
+ label: outcome.task.title,
54
40
  state: outcome.task.state, category: 'provision_failed',
55
41
  eventId: outcome.task.ended_at ?? room?.created_at ?? outcome.task.created_at,
56
42
  });
43
+ if (outcome.kind === 'in_progress')
44
+ recordFleetAuditPresentation({
45
+ kind: 'lifecycle_failure', resource: 'Task', id: outcome.task.task_id,
46
+ label: outcome.task.title,
47
+ state: outcome.task.state, category: 'provision_pending',
48
+ eventId: room?.created_at ?? outcome.task.created_at,
49
+ });
57
50
  }
58
51
  export class TaskRoomApplicationError extends Error {
59
52
  code;
@@ -73,7 +66,6 @@ export function resolveRoomLaunchPolicy(template, override) {
73
66
  export class TaskRoomApplicationService {
74
67
  configurationPath;
75
68
  deps;
76
- recovery;
77
69
  constructor(configurationPath, deps = {}) {
78
70
  this.configurationPath = configurationPath;
79
71
  this.deps = deps;
@@ -124,9 +116,9 @@ export class TaskRoomApplicationService {
124
116
  }
125
117
  unlock?.();
126
118
  const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
127
- // A create-and-start launch is represented to the Owner solely by the
128
- // canonical Room created/ready events. Backlog and no-room tasks retain
129
- // their useful Task lifecycle notice.
119
+ // A successful create-and-start launch is recorded once by
120
+ // recordTaskProvisioningOutcome after the linked Room and seats are ready.
121
+ // Backlog and no-room tasks retain their direct Task lifecycle notice.
130
122
  if (request.backlog || request.noRoom)
131
123
  recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
132
124
  title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
@@ -146,13 +138,15 @@ export class TaskRoomApplicationService {
146
138
  persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
147
139
  // Once the durable Room exists, provisioning errors are resumable
148
140
  // saga state. Return that explicit state so the command can launch a
149
- // continuation and give the caller a stable await handle.
141
+ // continuation and report the durable in-progress outcome.
150
142
  const current = readTask(task.task_id);
151
143
  if (!current.room_id || !getRoomRecord(current.room_id))
152
144
  throw error;
153
145
  task = current;
154
146
  }
155
147
  }
148
+ if (!request.backlog && !request.noRoom && task.room_id)
149
+ recordTaskProvisioningOutcome(this.taskProvisioningOutcome(task.task_id));
156
150
  return task;
157
151
  }
158
152
  async createRoom(request) {
@@ -177,7 +171,10 @@ export class TaskRoomApplicationService {
177
171
  }, template, () => { }, undefined, roomPolicy);
178
172
  }
179
173
  async startTask(input) {
180
- return (await this.ensureTaskWork(input)).task;
174
+ const result = await this.ensureTaskWork(input);
175
+ if (result.status !== 'already_active')
176
+ recordTaskProvisioningOutcome(this.taskProvisioningOutcome(result.task.task_id));
177
+ return result.task;
181
178
  }
182
179
  listTasks(filter) {
183
180
  const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
@@ -237,19 +234,18 @@ export class TaskRoomApplicationService {
237
234
  && active === expected && launched === expected;
238
235
  const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
239
236
  const nextAction = room?.provisioning_detail === 'waiting_owner_authorization'
240
- ? 'Restore ours-cowork, then await the same task to retry Owner command authorization.'
237
+ ? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
241
238
  : room?.provisioning_detail === 'waiting_owner_invite'
242
- ? 'Rotate rooms.owner.public_invite, then await the same task.'
239
+ ? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
243
240
  : room?.provisioning_detail === 'owner_cid_mismatch'
244
- ? 'Verify rooms.owner.expected_cid, rotate the Owner invite, then await the same task.'
241
+ ? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
245
242
  : room?.provisioning_detail === 'waiting_cowork'
246
- ? 'Restore ours-cowork, then await the same task.'
243
+ ? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
247
244
  : failed
248
- ? `Correct the blocker, then run ours-fleet task recover ${task.task_id}.`
245
+ ? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
249
246
  : undefined;
250
247
  return {
251
248
  kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
252
- handle: { command: `ours-fleet task await ${task.task_id}`, task_id: task.task_id },
253
249
  launch: {
254
250
  ...(room?.template_snapshot
255
251
  ? { template: `${room.template_snapshot.name}@${room.template_snapshot.version}` }
@@ -408,40 +404,14 @@ export class TaskRoomApplicationService {
408
404
  recordSettlementError(input) {
409
405
  return recordTaskTerminalIntentError(input.taskId, input.error, input.recoveryHint);
410
406
  }
411
- async beginTaskRecovery(input) {
412
- // The deletion-vs-terminal routing decision serializes on the common
413
- // task-operation lock and must not require configuration: recovery of a
414
- // deletion-pending task continues the deletion and never resurrects.
415
- const routed = await withFileLock(taskOperationLockPath(input.taskId), () => {
416
- const task = readTask(input.taskId);
417
- if (task.deletion?.status === 'pending')
418
- return 'deletion';
419
- if (task.terminal_intent?.status === 'pending')
420
- return 'terminal';
421
- return 'none';
422
- }, {}, TASK_OPERATION_LOCK_STALE_MS);
423
- if (routed === 'deletion')
424
- return { kind: 'deletion_worker_required', taskId: input.taskId };
425
- const config = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
426
- this.recovery = { taskId: input.taskId, config };
427
- if (routed === 'terminal')
428
- return { kind: 'terminal_worker_required', taskId: input.taskId };
429
- return { kind: 'final', result: await this.continueTaskRecovery({
430
- actor: input.actor, taskId: input.taskId, terminalTimedOut: false,
431
- }) };
432
- }
433
- async continueTaskRecovery(input) {
434
- if (!this.recovery || this.recovery.taskId !== input.taskId)
435
- throw new Error('task recovery continuation requires a matching begin');
436
- const recovery = this.recovery;
437
- this.recovery = undefined;
438
- const cfg = recovery.config;
407
+ async continueTaskProvisioning(input) {
408
+ const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
439
409
  let task = readTask(input.taskId);
440
410
  let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
441
- const issues = input.terminalTimedOut ? [{ code: 'terminal_pending' }] : [];
411
+ const issues = [];
442
412
  if (task.state !== 'provisioning')
443
413
  return {
444
- kind: TASK_TERMINAL_STATES.includes(task.state) ? 'terminal' : 'no_op', task, room, issues,
414
+ kind: 'no_op', task, room, issues,
445
415
  };
446
416
  if (!room) {
447
417
  if (!task.template)
@@ -470,7 +440,7 @@ export class TaskRoomApplicationService {
470
440
  || room.provisioning_detail === 'waiting_owner_invite'
471
441
  || room.provisioning_detail === 'owner_cid_mismatch') {
472
442
  try {
473
- await this.recoverRoom({ actor: input.actor, roomId: room.room_id });
443
+ await this.reconcileProvisioningOwner(cfg, room.room_id);
474
444
  task = readTask(task.task_id);
475
445
  room = getRoomRecord(room.room_id);
476
446
  if (task.state === 'active' && room?.state === 'active')
@@ -527,6 +497,50 @@ export class TaskRoomApplicationService {
527
497
  return { kind: 'provisioning_resume_failed', task, room, issues };
528
498
  }
529
499
  }
500
+ async reconcileProvisioningOwner(cfg, roomId) {
501
+ if (!cfg.rooms)
502
+ throw new ConfigError('rooms: configuration is required before creating or querying rooms');
503
+ const cowork = this.deps.cowork ? this.deps.cowork(cfg)
504
+ : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
505
+ const remote = await cowork.recoverRoom(roomId);
506
+ const room = getRoomRecord(roomId);
507
+ if (!room)
508
+ throw new Error(`room ${roomId} has no durable orchestration record`);
509
+ if (room.state === 'closing' || room.state === 'closed')
510
+ throw new Error(`room ${roomId} is already ${room.state}`);
511
+ const policy = storedRoomLaunchPolicy(room.room_policy);
512
+ if ((remote.anonymous ?? false) !== policy.anonymous) {
513
+ const error = `Cowork anonymity (${String(remote.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(policy.anonymous)})`;
514
+ setSagaError(roomId, error, 'Do not respawn members. Repair or upgrade Cowork, then retry the originating task.', 'waiting_cowork');
515
+ throw new Error(error);
516
+ }
517
+ if (room.owner_seat_cid) {
518
+ const ownerCid = room.owner_seat_cid.toLowerCase();
519
+ const ownerSeat = remote.seats.find(seat => seat.identity_cid.toLowerCase() === ownerCid && seat.seat_state !== 'removed');
520
+ if (ownerSeat)
521
+ await cowork.setRoleCommands(roomId, {
522
+ role: ownerSeat.role, commands: [...OWNER_ROOM_COMMANDS],
523
+ });
524
+ if (room.saga.phase === 'attach_owner')
525
+ advanceSaga(roomId, 'create_members', 3);
526
+ return;
527
+ }
528
+ const expected = cfg.rooms.owner.expected_cid?.toLowerCase();
529
+ if (!expected)
530
+ throw new ConfigError('rooms.owner.expected_cid is required before continuing Owner-seat provisioning');
531
+ const existing = (await cowork.getSeats(roomId))
532
+ .find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
533
+ if (!existing && !cfg.ownerInvite)
534
+ throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before continuing task provisioning');
535
+ await cowork.setRoleCommands(roomId, {
536
+ role: existing?.role ?? cfg.rooms.owner.role, commands: [...OWNER_ROOM_COMMANDS],
537
+ });
538
+ const acceptedCid = existing?.identity_cid ?? (await cowork.acceptInvite(roomId, cfg.ownerInvite, {
539
+ role: cfg.rooms.owner.role, expected_cid: cfg.rooms.owner.expected_cid,
540
+ })).seat_cid;
541
+ setOwnerSeat(roomId, acceptedCid, cfg.ownerInviteFingerprint ?? '');
542
+ advanceSaga(roomId, 'create_members', 3);
543
+ }
530
544
  async acceptTerminal(taskId, kind, roomId, outcome) {
531
545
  const task = await acceptTaskTerminalIntent({ taskId, kind, roomId, outcome });
532
546
  return { task, settlementRequired: task.terminal_intent?.status === 'pending' && !!roomId };
@@ -619,106 +633,6 @@ export class TaskRoomApplicationService {
619
633
  recordRoomSettlementError(input) {
620
634
  return recordManagedRoomCloseError(input.roomId, input.error, input.recoveryHint);
621
635
  }
622
- async recoverRoom(input) {
623
- const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
624
- if (!cfg.rooms)
625
- throw new ConfigError('rooms: configuration is required before creating or querying rooms');
626
- const adapter = this.deps.cowork ? this.deps.cowork(cfg)
627
- : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
628
- let orchestration = getRoomRecord(input.roomId);
629
- if (orchestration?.state === 'closing' || orchestration?.state === 'closed')
630
- return { kind: 'deletion_worker_required', roomId: input.roomId };
631
- const room = await adapter.recoverRoom(input.roomId);
632
- orchestration = getRoomRecord(input.roomId);
633
- if (orchestration) {
634
- const policy = storedRoomLaunchPolicy(orchestration.room_policy);
635
- if ((room.anonymous ?? false) !== policy.anonymous) {
636
- const error = `Cowork anonymity (${String(room.anonymous ?? false)}) does not match Fleet's durable Room policy (${String(policy.anonymous)})`;
637
- setSagaError(input.roomId, error, 'Do not respawn members. Repair or upgrade Cowork, then recover the Room without changing its policy.', 'waiting_cowork');
638
- throw new Error(error);
639
- }
640
- }
641
- if (orchestration?.owner_seat_cid) {
642
- const ownerCid = orchestration.owner_seat_cid.toLowerCase();
643
- const ownerSeat = room.seats.find(seat => seat.identity_cid.toLowerCase() === ownerCid && seat.seat_state !== 'removed');
644
- if (ownerSeat)
645
- await adapter.setRoleCommands(input.roomId, {
646
- role: ownerSeat.role, commands: [...OWNER_ROOM_COMMANDS],
647
- });
648
- }
649
- if (orchestration && !orchestration.owner_seat_cid
650
- && (orchestration.saga.phase === 'attach_owner'
651
- || orchestration.provisioning_detail === 'waiting_owner_authorization'
652
- || orchestration.provisioning_detail === 'waiting_owner_invite'
653
- || orchestration.provisioning_detail === 'owner_cid_mismatch')) {
654
- const expected = cfg.rooms.owner.expected_cid.toLowerCase();
655
- const existing = (await adapter.getSeats(input.roomId))
656
- .find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
657
- if (!existing && !cfg.ownerInvite)
658
- throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
659
- await adapter.setRoleCommands(input.roomId, {
660
- role: existing?.role ?? cfg.rooms.owner.role, commands: [...OWNER_ROOM_COMMANDS],
661
- });
662
- let acceptedCid = existing?.identity_cid;
663
- if (!acceptedCid)
664
- acceptedCid = (await adapter.acceptInvite(input.roomId, cfg.ownerInvite, {
665
- role: cfg.rooms.owner.role, expected_cid: cfg.rooms.owner.expected_cid,
666
- })).seat_cid;
667
- setOwnerSeat(input.roomId, acceptedCid, cfg.ownerInviteFingerprint ?? '');
668
- orchestration = advanceSaga(input.roomId, 'create_members', 3);
669
- }
670
- const issues = [];
671
- if (orchestration?.saga.error)
672
- issues.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
673
- if (orchestration?.saga.recovery_hint)
674
- issues.push('Recovery guidance is recorded; inspect role logs for diagnostics.');
675
- if (orchestration?.provisioning_detail === 'waiting_cowork')
676
- issues.push('Check ours-cowork service status');
677
- if (orchestration?.provisioning_detail === 'waiting_owner_authorization')
678
- issues.push('Restore Cowork availability, then re-run recover');
679
- if (orchestration?.provisioning_detail === 'waiting_owner_invite')
680
- issues.push('Rotate rooms.owner.public_invite in config, then re-run recover');
681
- if (orchestration?.provisioning_detail === 'waiting_seats')
682
- issues.push('Inspect temporary member logs for invite acceptance, then re-run recover');
683
- if (orchestration?.state === 'provisioning'
684
- && ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(orchestration.saga.phase)
685
- && orchestration.template_snapshot) {
686
- try {
687
- let template = orchestration.template_snapshot;
688
- let brief;
689
- let goal = orchestration.room_name;
690
- if (orchestration.task_id) {
691
- const task = readTask(orchestration.task_id);
692
- if (!task.template || task.template.name !== template.name || task.template.version !== template.version
693
- || task.template.content_hash !== template.content_hash)
694
- throw new Error(`task ${task.task_id} template reference does not match room ${orchestration.room_id}'s durable snapshot`);
695
- brief = task.brief;
696
- goal = task.title;
697
- }
698
- await (this.deps.provisionMembers ?? provisionMembers)({ cfg, cowork: adapter,
699
- roomId: orchestration.room_id, taskId: orchestration.task_id, template,
700
- binPath: (this.deps.binPath ?? getBinPath)(), brief, goal });
701
- orchestration = getRoomRecord(input.roomId);
702
- return { kind: 'provisioning_resumed', room, orchestration,
703
- issues: ['Provisioning resumed successfully'] };
704
- }
705
- catch (error) {
706
- issues.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
707
- return { kind: 'provisioning_resume_failed', room, orchestration, issues };
708
- }
709
- }
710
- if (orchestration?.state === 'provisioning' && !orchestration.template_snapshot) {
711
- const seats = await adapter.getSeats(input.roomId);
712
- const ownerReady = !orchestration.owner_seat_cid || seats.some(seat => seat.identity_cid.toLowerCase() === orchestration.owner_seat_cid.toLowerCase()
713
- && seat.seat_state === 'active');
714
- if (ownerReady) {
715
- orchestration = activateRoom(input.roomId);
716
- if (orchestration.task_id)
717
- activateTask(orchestration.task_id);
718
- }
719
- }
720
- return { kind: 'recovered', room, orchestration, issues };
721
- }
722
636
  async finishTask(input) {
723
637
  let task = readTask(input.taskId);
724
638
  if (TASK_TERMINAL_STATES.includes(task.state))
@@ -769,7 +683,7 @@ export class TaskRoomApplicationService {
769
683
  }
770
684
  return { task, status: 'already_active' };
771
685
  }
772
- const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
686
+ let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
773
687
  const durable = room?.template_snapshot ?? task.execution_plan?.snapshot;
774
688
  if (durable && (!task.template || task.template.name !== durable.name
775
689
  || task.template.version !== durable.version || task.template.content_hash !== durable.content_hash))
@@ -872,6 +786,21 @@ export class TaskRoomApplicationService {
872
786
  }
873
787
  }
874
788
  else if (task.state === 'provisioning') {
789
+ if (room && (room.saga.phase === 'attach_owner'
790
+ || room.provisioning_detail === 'waiting_owner_authorization'
791
+ || room.provisioning_detail === 'waiting_owner_invite'
792
+ || room.provisioning_detail === 'owner_cid_mismatch')) {
793
+ try {
794
+ await this.reconcileProvisioningOwner(cfg, room.room_id);
795
+ room = getRoomRecord(room.room_id);
796
+ }
797
+ catch (error) {
798
+ if (error instanceof CoworkUnavailableError)
799
+ persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
800
+ task = readTask(task.task_id);
801
+ return { task, status: 'in_progress' };
802
+ }
803
+ }
875
804
  if (!room || !['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(room.saga.phase))
876
805
  throw new TaskRoomApplicationError('task_non_resumable', 'task non-resumable', { task: task.task_id, room: task.room_id });
877
806
  try {
@@ -967,10 +896,6 @@ export class TaskRoomApplicationService {
967
896
  throw error;
968
897
  }
969
898
  unlockSnapshot?.();
970
- recordCanonicalRoomCreated(room);
971
- // The created notice is observable while member admission is still in
972
- // progress; it is not delayed behind task-start command completion.
973
- await checkpointFleetAuditPresentations();
974
899
  room = advanceSaga(room.room_id, 'create_room', 1);
975
900
  if (attachOwner) {
976
901
  room = advanceSaga(room.room_id, 'attach_owner', 2);
@@ -980,7 +905,7 @@ export class TaskRoomApplicationService {
980
905
  });
981
906
  }
982
907
  catch (error) {
983
- setSagaError(room.room_id, error instanceof Error ? error.message : String(error), 'Restore Cowork availability, then run room recover to configure Owner command authorization.', 'waiting_owner_authorization');
908
+ setSagaError(room.room_id, error instanceof Error ? error.message : String(error), 'Restore Cowork availability, then retry the originating task or room create command.', 'waiting_owner_authorization');
984
909
  throw error;
985
910
  }
986
911
  try {
@@ -992,7 +917,11 @@ export class TaskRoomApplicationService {
992
917
  catch (error) {
993
918
  const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
994
919
  setSagaError(room.room_id, error instanceof Error ? error.message : String(error), mismatch ? 'Verify rooms.owner.expected_cid and rotate the configured invite if necessary.'
995
- : 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
920
+ : 'Rotate rooms.owner.public_invite or public_invite_file, then retry the originating task or room create command.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
921
+ if (!task.task_id)
922
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
923
+ id: room.room_id, label: room.room_name, state: room.state, category: 'provision_failed',
924
+ eventId: room.created_at });
996
925
  throw error;
997
926
  }
998
927
  }
@@ -1006,6 +935,10 @@ export class TaskRoomApplicationService {
1006
935
  });
1007
936
  }
1008
937
  catch (error) {
938
+ if (!task.task_id)
939
+ recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
940
+ id: room.room_id, label: room.room_name, state: room.state, category: 'provision_failed',
941
+ eventId: room.created_at });
1009
942
  throw error;
1010
943
  }
1011
944
  else if (attachOwner) {
@@ -1028,14 +961,20 @@ export class TaskRoomApplicationService {
1028
961
  }
1029
962
  else
1030
963
  room = activateRoom(room.room_id);
1031
- if (room.state === 'active')
1032
- recordFleetAuditPresentation({ kind: 'room', operation: 'activate',
1033
- eventId: `room-ready:${room.activated_at ?? room.created_at}`, id: room.room_id,
1034
- name: room.room_name, previousState: 'provisioning', newState: 'active',
1035
- revision: room.activated_at ?? room.created_at, taskId: room.task_id,
964
+ if (!task.task_id && room.state === 'active')
965
+ recordFleetAuditPresentation({
966
+ kind: 'room', operation: 'activate', eventId: `room-ready:${room.activated_at ?? room.created_at}`,
967
+ id: room.room_id, name: room.room_name, previousState: 'provisioning', newState: 'active',
968
+ revision: room.activated_at ?? room.created_at,
1036
969
  template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
1037
970
  anonymous: policy.anonymous, memberCount: room.member_seats.length, ownerAttached: attachOwner,
1038
- participants: [] });
971
+ participants: [],
972
+ });
973
+ if (!task.task_id && room.state !== 'active')
974
+ recordFleetAuditPresentation({
975
+ kind: 'lifecycle_failure', resource: 'Room', id: room.room_id, label: room.room_name,
976
+ state: room.state, category: 'provision_pending', eventId: room.created_at,
977
+ });
1039
978
  if (task.task_id && !launchTemplate?.members.length)
1040
979
  activateTask(task.task_id);
1041
980
  return room;
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.1.0-nightly.28",
3
- "buildId": "761c569df66f",
4
- "commit": "c03ee4d380feb9e12fcc503785f3d81203026c2a",
2
+ "version": "1.1.0-nightly.29",
3
+ "buildId": "4c492a1ee52e",
4
+ "commit": "b201f3f81183766a67423d34c3acd938ddd2d115",
5
5
  "dirty": true,
6
- "builtAt": "2026-09-04T14:31:02.540Z",
6
+ "builtAt": "2026-09-04T16:18:42.272Z",
7
7
  "capabilities": [
8
8
  "monitor.interrupt.after_tool"
9
9
  ]