@ours.network/fleet 1.1.0-nightly.9 → 1.1.0

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.
Files changed (200) hide show
  1. package/README.md +231 -46
  2. package/dist/application/capabilities.js +18 -6
  3. package/dist/application/fleet-query-service.js +7 -4
  4. package/dist/application/role-command-service.js +9 -0
  5. package/dist/application/role-creation-service.d.ts +4 -4
  6. package/dist/application/role-creation-service.js +8 -1
  7. package/dist/application/role-repository.d.ts +4 -2
  8. package/dist/application/role-repository.js +6 -3
  9. package/dist/application/session-control.d.ts +3 -1
  10. package/dist/application/session-control.js +4 -2
  11. package/dist/application/task-room-service.d.ts +92 -39
  12. package/dist/application/task-room-service.js +562 -289
  13. package/dist/briefing.js +79 -22
  14. package/dist/build-info.json +5 -5
  15. package/dist/canonical-json.d.ts +2 -0
  16. package/dist/canonical-json.js +10 -0
  17. package/dist/cli.js +129 -47
  18. package/dist/config.d.ts +38 -3
  19. package/dist/config.js +193 -28
  20. package/dist/creation.d.ts +1 -1
  21. package/dist/creation.js +7 -4
  22. package/dist/docs.d.ts +1 -1
  23. package/dist/docs.js +181 -41
  24. package/dist/doctor.d.ts +2 -0
  25. package/dist/doctor.js +40 -4
  26. package/dist/fleet-command-audit.d.ts +22 -1
  27. package/dist/fleet-command-audit.js +355 -50
  28. package/dist/fleet-proxy.d.ts +5 -2
  29. package/dist/harness/claude-code-session.js +2 -1
  30. package/dist/harness/codex-app-server-proxy.d.ts +40 -0
  31. package/dist/harness/codex-app-server-proxy.js +348 -14
  32. package/dist/harness/codex-session.d.ts +9 -2
  33. package/dist/harness/codex-session.js +18 -2
  34. package/dist/harness/codex.d.ts +5 -1
  35. package/dist/harness/codex.js +104 -10
  36. package/dist/harness/types.d.ts +1 -1
  37. package/dist/index.d.ts +4 -3
  38. package/dist/index.js +3 -2
  39. package/dist/init-guidance.d.ts +1 -1
  40. package/dist/init-guidance.js +1 -1
  41. package/dist/init-wizard.d.ts +98 -0
  42. package/dist/init-wizard.js +631 -0
  43. package/dist/lifecycle-summary.d.ts +134 -0
  44. package/dist/lifecycle-summary.js +179 -0
  45. package/dist/loops/config.d.ts +10 -1
  46. package/dist/loops/config.js +38 -5
  47. package/dist/model-recovery.d.ts +1 -1
  48. package/dist/ops.js +2 -17
  49. package/dist/owner-channel/channel.d.ts +19 -3
  50. package/dist/owner-channel/channel.js +301 -120
  51. package/dist/owner-channel/commands.d.ts +28 -6
  52. package/dist/owner-channel/commands.js +109 -27
  53. package/dist/owner-channel/lifecycle-outbox.d.ts +24 -0
  54. package/dist/owner-channel/lifecycle-outbox.js +87 -0
  55. package/dist/owner-channel/ours-client.d.ts +19 -0
  56. package/dist/owner-channel/ours-client.js +3 -0
  57. package/dist/permissions.js +1 -1
  58. package/dist/preset-migration.d.ts +42 -0
  59. package/dist/preset-migration.js +450 -0
  60. package/dist/reports/artifact.d.ts +10 -0
  61. package/dist/reports/artifact.js +32 -0
  62. package/dist/reports/index.d.ts +5 -0
  63. package/dist/reports/index.js +5 -0
  64. package/dist/reports/render.d.ts +6 -0
  65. package/dist/reports/render.js +240 -0
  66. package/dist/reports/service.d.ts +42 -0
  67. package/dist/reports/service.js +57 -0
  68. package/dist/reports/tasks.d.ts +52 -0
  69. package/dist/reports/tasks.js +177 -0
  70. package/dist/reports/types.d.ts +315 -0
  71. package/dist/reports/types.js +6 -0
  72. package/dist/resolved-plan.js +13 -1
  73. package/dist/rooms-tasks/cli.d.ts +2 -0
  74. package/dist/rooms-tasks/cli.js +361 -285
  75. package/dist/rooms-tasks/close.d.ts +9 -1
  76. package/dist/rooms-tasks/close.js +23 -7
  77. package/dist/rooms-tasks/config.js +16 -25
  78. package/dist/rooms-tasks/cowork-adapter.d.ts +5 -0
  79. package/dist/rooms-tasks/cowork-adapter.js +13 -0
  80. package/dist/rooms-tasks/deletion.d.ts +51 -0
  81. package/dist/rooms-tasks/deletion.js +217 -0
  82. package/dist/rooms-tasks/external-worker.d.ts +8 -0
  83. package/dist/rooms-tasks/external-worker.js +38 -0
  84. package/dist/rooms-tasks/launch-snapshot.d.ts +19 -0
  85. package/dist/rooms-tasks/launch-snapshot.js +181 -0
  86. package/dist/rooms-tasks/member-overrides.d.ts +30 -0
  87. package/dist/rooms-tasks/member-overrides.js +204 -0
  88. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  89. package/dist/rooms-tasks/member-startup.js +9 -2
  90. package/dist/rooms-tasks/provision.js +146 -83
  91. package/dist/rooms-tasks/room-state.d.ts +1 -0
  92. package/dist/rooms-tasks/room-state.js +12 -1
  93. package/dist/rooms-tasks/task-room-name.d.ts +12 -0
  94. package/dist/rooms-tasks/task-room-name.js +61 -0
  95. package/dist/rooms-tasks/task-state.d.ts +115 -3
  96. package/dist/rooms-tasks/task-state.js +361 -11
  97. package/dist/rooms-tasks/templates.d.ts +4 -1
  98. package/dist/rooms-tasks/templates.js +34 -3
  99. package/dist/rooms-tasks/terminal.d.ts +3 -0
  100. package/dist/rooms-tasks/terminal.js +10 -4
  101. package/dist/rooms-tasks/types.d.ts +92 -7
  102. package/dist/rooms-tasks/types.js +14 -1
  103. package/dist/runner.d.ts +3 -0
  104. package/dist/runner.js +91 -44
  105. package/dist/session/acp.d.ts +4 -1
  106. package/dist/session/acp.js +29 -13
  107. package/dist/session/arbiter.d.ts +2 -1
  108. package/dist/session/arbiter.js +2 -0
  109. package/dist/session/codex-app-server-transport.d.ts +53 -0
  110. package/dist/session/codex-app-server-transport.js +184 -0
  111. package/dist/session/codex-app-server.d.ts +122 -0
  112. package/dist/session/codex-app-server.js +1031 -0
  113. package/dist/session/control.d.ts +1 -1
  114. package/dist/session/control.js +17 -1
  115. package/dist/session/conversation-types.d.ts +4 -4
  116. package/dist/session/types.d.ts +24 -6
  117. package/dist/session/types.js +28 -0
  118. package/dist/spawn.d.ts +9 -0
  119. package/dist/spawn.js +77 -9
  120. package/dist/web/fleet-config-service.d.ts +1 -0
  121. package/dist/web/fleet-config-service.js +25 -7
  122. package/dist/web/runtime.js +11 -6
  123. package/dist/web/server.js +28 -1
  124. package/dist/web-app/assets/{index-BbTc5KtZ.js → index-C0h3ALvs.js} +8 -8
  125. package/dist/web-app/index.html +1 -1
  126. package/package.json +4 -4
  127. package/presets/brain-catalog.json +17 -0
  128. package/presets/fleet/{agents → agent_templates}/Critic.yaml +2 -0
  129. package/presets/fleet/{agents/Agent.yaml → agent_templates/Developer.yaml} +3 -1
  130. package/presets/fleet/agent_templates/LocalCoordinator.yaml +50 -0
  131. package/presets/fleet/agents/{Developer.yaml → FleetCoordinator.yaml} +1 -1
  132. package/presets/fleet/brains/claude-fable-5-high.yaml +5 -0
  133. package/presets/fleet/brains/claude-fable-5-low.yaml +5 -0
  134. package/presets/fleet/brains/claude-fable-5-max.yaml +5 -0
  135. package/presets/fleet/brains/claude-fable-5-medium.yaml +5 -0
  136. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +5 -0
  137. package/presets/fleet/brains/claude-opus-5-high.yaml +5 -0
  138. package/presets/fleet/brains/claude-opus-5-low.yaml +5 -0
  139. package/presets/fleet/brains/claude-opus-5-max.yaml +5 -0
  140. package/presets/fleet/brains/claude-opus-5-medium.yaml +5 -0
  141. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +5 -0
  142. package/presets/fleet/brains/claude-sonnet-5-high.yaml +5 -0
  143. package/presets/fleet/brains/claude-sonnet-5-low.yaml +5 -0
  144. package/presets/fleet/brains/claude-sonnet-5-max.yaml +5 -0
  145. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +5 -0
  146. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +5 -0
  147. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +5 -0
  148. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +5 -0
  149. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +5 -0
  150. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +5 -0
  151. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +5 -0
  152. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +5 -0
  153. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +5 -0
  154. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +5 -0
  155. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +5 -0
  156. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +5 -0
  157. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +5 -0
  158. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +5 -0
  159. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +5 -0
  160. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +5 -0
  161. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +5 -0
  162. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +5 -0
  163. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +5 -0
  164. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +5 -0
  165. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +5 -0
  166. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +5 -0
  167. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +5 -0
  168. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +5 -0
  169. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +5 -0
  170. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +5 -0
  171. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +5 -0
  172. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +5 -0
  173. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +5 -0
  174. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +5 -0
  175. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +5 -0
  176. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +5 -0
  177. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +5 -0
  178. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +5 -0
  179. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +5 -0
  180. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +5 -0
  181. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +5 -0
  182. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +5 -0
  183. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +5 -0
  184. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +5 -0
  185. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +5 -0
  186. package/presets/fleet/roles/Coordinator.yaml +37 -0
  187. package/presets/fleet/roles/Critic.yaml +23 -1
  188. package/presets/fleet/roles/Developer.yaml +27 -5
  189. package/presets/fleet/roles/LocalCoordinator.yaml +35 -0
  190. package/presets/fleet/room_templates/pair.yaml +9 -7
  191. package/presets/fleet/room_templates/single.yaml +5 -4
  192. package/presets/fleet/room_templates/team.yaml +12 -8
  193. package/presets/manifest.json +2 -1
  194. package/presets/fleet/agents/Architect.yaml +0 -3
  195. package/presets/fleet/agents/Secretary.yaml +0 -3
  196. package/presets/fleet/agents/Tester.yaml +0 -3
  197. package/presets/fleet/roles/Agent.yaml +0 -6
  198. package/presets/fleet/roles/Architect.yaml +0 -6
  199. package/presets/fleet/roles/Secretary.yaml +0 -6
  200. package/presets/fleet/roles/Tester.yaml +0 -6
@@ -1,16 +1,53 @@
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';
6
5
  import { activateRoom, advanceSaga, createRoomRecord, getRoomRecord, listRoomRecords, setOwnerSeat, setSagaError, } from '../rooms-tasks/room-state.js';
7
- import { activateTask, blockTask as persistBlockTask, createTask as persistTask, deleteTask as persistDeleteTask, getTask as readTask, listTasks as readTasks, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, } from '../rooms-tasks/task-state.js';
6
+ import { activateTask, blockTask as persistBlockTask, createTask as persistTask, getDeletingTask, getTask as readTask, listTasks as readTasks, taskDeletionState, moveTaskToList, reviewTask as persistReviewTask, startTask as transitionTask, TaskStateError, unblockTask as persistUnblockTask, updateTaskRoom, updateTaskTemplate, updateTaskExecutionPlan, } from '../rooms-tasks/task-state.js';
8
7
  import { createTaskListLocked, DEFAULT_TASK_LIST_ID, deleteTaskListRecordLocked, readTaskLists, renameTaskListLocked, resolveTaskList, TaskListError, withTaskListsLock, } from '../rooms-tasks/task-lists.js';
9
- import { recordFleetAuditPresentation } from '../fleet-command-audit.js';
10
- import { hashTemplate, listTemplates, resolveTemplate, snapshotTemplate } from '../rooms-tasks/templates.js';
8
+ import { hashTemplate, listTemplates, resolveTemplate, sealTemplateSnapshot, snapshotTemplate } from '../rooms-tasks/templates.js';
9
+ import { acquireLaunchSnapshotLock, releaseLaunchSnapshot } from '../rooms-tasks/launch-snapshot.js';
10
+ import { hashMemberOverrides, prepareExecutionPlan } from '../rooms-tasks/member-overrides.js';
11
+ import { recordFleetAuditPresentation, recordFleetAuditResource, } from '../fleet-command-audit.js';
11
12
  import { acceptManagedRoomClose, deleteLegacyClosedRooms, deleteManagedRoom, recordManagedRoomCloseError, } from '../rooms-tasks/close.js';
12
- import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, } from '../rooms-tasks/terminal.js';
13
- import { TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
13
+ import { acceptTaskTerminalIntent, recordTaskTerminalIntentError, settleTaskTerminalIntent, TASK_OPERATION_LOCK_STALE_MS, taskOperationLockPath, } from '../rooms-tasks/terminal.js';
14
+ import { acceptTaskDeletion, recordTaskDeletionError, settleTaskDeletion, } from '../rooms-tasks/deletion.js';
15
+ import { withFileLock } from '../atomic-file.js';
16
+ import { launchFleetWorker } from '../rooms-tasks/external-worker.js';
17
+ import { storedRoomLaunchPolicy, TASK_CANCELLABLE_STATES, TASK_TERMINAL_STATES } from '../rooms-tasks/types.js';
18
+ import { deriveTaskRoomName } from '../rooms-tasks/task-room-name.js';
19
+ const OWNER_ROOM_COMMANDS = ['list-members', 'remove-member'];
20
+ /** Repairable terminal observation derived entirely from durable Task/Room state. */
21
+ export function recordTaskProvisioningOutcome(outcome) {
22
+ const room = outcome.room;
23
+ recordFleetAuditResource('task', outcome.task.task_id);
24
+ if (room)
25
+ recordFleetAuditResource('room', room.room_id);
26
+ if (outcome.kind === 'ready' && room)
27
+ recordFleetAuditPresentation({
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 })),
35
+ });
36
+ if (outcome.kind === 'failed')
37
+ recordFleetAuditPresentation({
38
+ kind: 'lifecycle_failure', resource: 'Task', id: outcome.task.task_id,
39
+ label: outcome.task.title,
40
+ state: outcome.task.state, category: 'provision_failed',
41
+ eventId: outcome.task.ended_at ?? room?.created_at ?? outcome.task.created_at,
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
+ });
50
+ }
14
51
  export class TaskRoomApplicationError extends Error {
15
52
  code;
16
53
  fields;
@@ -21,102 +58,95 @@ export class TaskRoomApplicationError extends Error {
21
58
  this.name = 'TaskRoomApplicationError';
22
59
  }
23
60
  }
24
- function launchSelection(definition, key) {
25
- const selected = definition?.[key];
26
- if (!selected || typeof selected !== 'object' || Array.isArray(selected))
27
- return undefined;
28
- const value = selected;
29
- if (value.kind === 'ref' && typeof value.id === 'string')
30
- return `ref:${value.id} (reference)`;
31
- if (value.kind === 'inline' && typeof value.fingerprint === 'string')
32
- return `inline:${value.fingerprint} (inline)`;
33
- return undefined;
34
- }
35
- function launchPermissions(definition) {
36
- const selected = definition?.permissions;
37
- if (!selected || typeof selected !== 'object' || Array.isArray(selected))
38
- return undefined;
39
- const value = selected;
40
- const fields = ['approval', 'filesystem', 'unattended'].flatMap(key => typeof value[key] === 'string' ? [`${key}=${value[key]}`] : []);
41
- return fields.length ? fields.join(',') : undefined;
42
- }
43
- function roomParticipants(room) {
44
- return [...room.member_seats].sort((a, b) => a.role_name.localeCompare(b.role_name)).map(seat => ({
45
- name: seat.role_name, id: seat.identity_cid,
46
- brain: launchSelection(seat.launch?.agent_definition, 'brain'),
47
- role: launchSelection(seat.launch?.agent_definition, 'role') ?? seat.cowork_role,
48
- permissions: launchPermissions(seat.launch?.agent_definition),
49
- }));
50
- }
51
- function taskAgents(task) {
52
- const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
53
- const seats = new Map(room?.member_seats.map(seat => [seat.role_name, seat]) ?? []);
54
- return [...task.member_roles].sort((a, b) => a.name.localeCompare(b.name)).map(member => {
55
- const seat = seats.get(member.name);
56
- return { name: member.name, brain: launchSelection(seat?.launch?.agent_definition, 'brain'),
57
- role: launchSelection(seat?.launch?.agent_definition, 'role') ?? member.cowork_role,
58
- permissions: launchPermissions(seat?.launch?.agent_definition) };
59
- });
60
- }
61
- function roomFailureEventId(room) {
62
- const evidence = `${room.created_at}\0${room.saga.phase}\0${room.saga.step_index}\0${room.saga.error ?? ''}`;
63
- return `sha256:${createHash('sha256').update(evidence).digest('hex')}`;
61
+ export function resolveRoomLaunchPolicy(template, override) {
62
+ const anonymous = override ?? template?.room?.anonymous ?? false;
63
+ return { anonymous };
64
64
  }
65
65
  /** Exact extraction of the previously CLI-owned task create/start behavior. */
66
66
  export class TaskRoomApplicationService {
67
67
  configurationPath;
68
68
  deps;
69
- recovery;
70
69
  constructor(configurationPath, deps = {}) {
71
70
  this.configurationPath = configurationPath;
72
71
  this.deps = deps;
73
72
  }
74
73
  async createTask(request) {
75
74
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
76
- const template = this.createTemplate(cfg, request.template, request.noRoom);
77
- const ref = template && {
78
- name: template.name, version: template.version, content_hash: template.content_hash,
79
- };
75
+ let template = this.createTemplate(cfg, request.template, request.noRoom);
76
+ if (request.noRoom && request.anonymous !== undefined)
77
+ throw new ConfigError('--anonymous/--no-anonymous cannot be combined with --no-room');
78
+ const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
79
+ let launchDefinitions;
80
+ let executionPlan;
81
+ let preparedPlan;
82
+ if (template) {
83
+ const definition = resolveTemplate(template.name, cfg.roomTemplates ?? {});
84
+ preparedPlan = prepareExecutionPlan(definition, cfg, request.members ?? {});
85
+ launchDefinitions = preparedPlan.launchDefinitions;
86
+ }
80
87
  const brief = request.briefFile ? readFileSync(request.briefFile, 'utf8') : request.brief;
81
- let task = await withTaskListsLock(() => {
82
- const list = resolveTaskList(request.list ?? 'default');
83
- return persistTask({
84
- title: request.title, brief, brief_file: request.briefFile, template: ref,
85
- origin: request.origin, idempotency_key: request.idempotencyKey,
86
- start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
88
+ const unlock = preparedPlan ? acquireLaunchSnapshotLock() : undefined;
89
+ let sealedHash;
90
+ let task;
91
+ try {
92
+ if (preparedPlan) {
93
+ template = sealTemplateSnapshot(preparedPlan.snapshot, cfg.agentTemplates ?? {}, preparedPlan.launchDefinitions);
94
+ sealedHash = template.launch_snapshot_hash;
95
+ executionPlan = { schema_version: 1, snapshot: template, room_policy: roomPolicy,
96
+ overrides: preparedPlan.overrides,
97
+ overrides_hash: preparedPlan.overridesHash,
98
+ plan_hash: preparedPlan.planHash };
99
+ }
100
+ const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
101
+ task = await withTaskListsLock(() => {
102
+ const list = resolveTaskList(request.list ?? 'default');
103
+ return persistTask({
104
+ title: request.title, brief, brief_file: request.briefFile, template: ref,
105
+ execution_plan: executionPlan,
106
+ origin: request.origin, idempotency_key: request.idempotencyKey,
107
+ start: !request.backlog, no_room: request.noRoom, listId: list.list_id,
108
+ });
87
109
  });
88
- });
89
- recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
90
- title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
91
- list: task.list_name ?? 'default', roomId: task.room_id,
92
- template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
93
- agents: [] });
110
+ }
111
+ catch (error) {
112
+ unlock?.();
113
+ if (sealedHash)
114
+ releaseLaunchSnapshot(sealedHash);
115
+ throw error;
116
+ }
117
+ unlock?.();
118
+ const ref = template && { name: template.name, version: template.version, content_hash: template.content_hash };
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.
122
+ if (request.backlog || request.noRoom)
123
+ recordFleetAuditPresentation({ kind: 'task', operation: 'create', id: task.task_id,
124
+ title: task.title, previousState: 'none', newState: task.state, revision: task.created_at,
125
+ list: task.list_name ?? 'default', roomId: task.room_id,
126
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
127
+ agents: [] });
94
128
  if (!request.backlog && !request.noRoom && ref && !task.room_id) {
95
129
  try {
96
130
  const room = await this.provisionRoom(cfg, task, template, created => {
97
131
  task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
98
- });
132
+ }, launchDefinitions, roomPolicy);
99
133
  task = readTask(task.task_id);
100
- if (task.state === 'active' && room.state === 'active')
101
- recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
102
- title: task.title, previousState: 'provisioning', newState: task.state,
103
- revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
104
- list: task.list_name ?? 'default', roomId: task.room_id,
105
- template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
106
- agents: taskAgents(task) });
107
- else
108
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
109
- state: task.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
134
+ void room;
110
135
  }
111
136
  catch (error) {
112
137
  if (error instanceof CoworkUnavailableError)
113
138
  persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
114
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
115
- id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
116
- eventId: task.room_id ? roomFailureEventId(getRoomRecord(task.room_id)) : task.created_at });
117
- throw error;
139
+ // Once the durable Room exists, provisioning errors are resumable
140
+ // saga state. Return that explicit state so the command can launch a
141
+ // continuation and report the durable in-progress outcome.
142
+ const current = readTask(task.task_id);
143
+ if (!current.room_id || !getRoomRecord(current.room_id))
144
+ throw error;
145
+ task = current;
118
146
  }
119
147
  }
148
+ if (!request.backlog && !request.noRoom && task.room_id)
149
+ recordTaskProvisioningOutcome(this.taskProvisioningOutcome(task.task_id));
120
150
  return task;
121
151
  }
122
152
  async createRoom(request) {
@@ -127,51 +157,28 @@ export class TaskRoomApplicationService {
127
157
  const definition = resolveTemplate(request.template, cfg.roomTemplates ?? {});
128
158
  if (!definition)
129
159
  throw new TaskRoomApplicationError('template_not_found', `template not found: ${request.template}`, { template: request.template });
130
- template = snapshotTemplate(definition);
160
+ if (request.members && Object.keys(request.members).length) {
161
+ const prepared = prepareExecutionPlan(definition, cfg, request.members);
162
+ template = prepared.snapshot;
163
+ const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
164
+ return this.provisionRoom(cfg, { title: request.name, brief, goal: request.goal }, template, () => { }, prepared.launchDefinitions, roomPolicy);
165
+ }
166
+ template = snapshotTemplate(definition, cfg.agentTemplates);
131
167
  }
168
+ const roomPolicy = resolveRoomLaunchPolicy(template, request.anonymous);
132
169
  return this.provisionRoom(cfg, {
133
170
  title: request.name, brief, goal: request.goal,
134
- }, template, () => { });
171
+ }, template, () => { }, undefined, roomPolicy);
135
172
  }
136
173
  async startTask(input) {
137
- const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
138
- let task = transitionTask(input.taskId);
139
- recordFleetAuditPresentation({ kind: 'task', operation: 'start', id: task.task_id,
140
- title: task.title, previousState: 'backlog', newState: task.state,
141
- revision: task.started_at ?? task.created_at, list: task.list_name ?? 'default',
142
- template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
143
- agents: [] });
144
- if (task.template && !task.room_id) {
145
- const template = this.existingTemplate(cfg, task);
146
- try {
147
- const room = await this.provisionRoom(cfg, task, template, created => {
148
- task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
149
- });
150
- task = readTask(task.task_id);
151
- if (task.state === 'active' && room.state === 'active')
152
- recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
153
- title: task.title, previousState: 'provisioning', newState: task.state,
154
- revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
155
- list: task.list_name ?? 'default', roomId: task.room_id,
156
- template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
157
- agents: taskAgents(task) });
158
- else
159
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
160
- state: task.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
161
- }
162
- catch (error) {
163
- const failed = task.room_id ? getRoomRecord(task.room_id) : undefined;
164
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task', id: task.task_id,
165
- state: readTask(task.task_id).state, category: 'provision_failed',
166
- eventId: failed ? roomFailureEventId(failed) : task.created_at });
167
- throw error;
168
- }
169
- }
170
- return 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;
171
178
  }
172
179
  listTasks(filter) {
173
180
  const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
174
- return readTasks({ state: filter?.state, listId });
181
+ return readTasks({ state: filter?.state, listId, includeDeleting: filter?.includeDeleting });
175
182
  }
176
183
  listTaskLists() { return readTaskLists(); }
177
184
  groupedTasks(filter) {
@@ -215,6 +222,55 @@ export class TaskRoomApplicationService {
215
222
  const task = readTask(taskId);
216
223
  return { task, orchestration: task.room_id ? getRoomRecord(task.room_id) : undefined };
217
224
  }
225
+ taskProvisioningOutcome(taskId) {
226
+ const task = readTask(taskId);
227
+ const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
228
+ const expected = room?.template_snapshot?.members.reduce((sum, member) => sum + member.count, 0)
229
+ ?? task.execution_plan?.snapshot.members.reduce((sum, member) => sum + member.count, 0) ?? 0;
230
+ const active = room?.member_seats.filter(seat => seat.seat_state === 'active').length ?? 0;
231
+ const launched = room?.member_seats.filter(seat => seat.launch?.state === 'launched').length ?? 0;
232
+ const failed = task.state === 'failed' || room?.saga.phase === 'failed';
233
+ const ready = task.state === 'active' && room?.state === 'active'
234
+ && active === expected && launched === expected;
235
+ const blocker = task.outcome?.summary ?? task.blocked?.reason ?? room?.saga.error;
236
+ const nextAction = room?.provisioning_detail === 'waiting_owner_authorization'
237
+ ? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
238
+ : room?.provisioning_detail === 'waiting_owner_invite'
239
+ ? `Rotate rooms.owner.public_invite, then run ours-fleet task start ${task.task_id}.`
240
+ : room?.provisioning_detail === 'owner_cid_mismatch'
241
+ ? `Verify rooms.owner.expected_cid, rotate the Owner invite, then run ours-fleet task start ${task.task_id}.`
242
+ : room?.provisioning_detail === 'waiting_cowork'
243
+ ? `Restore ours-cowork, then run ours-fleet task start ${task.task_id}.`
244
+ : failed
245
+ ? `Correct the blocker, then run ours-fleet task start ${task.task_id}.`
246
+ : undefined;
247
+ return {
248
+ kind: failed ? 'failed' : ready ? 'ready' : 'in_progress', task, room,
249
+ launch: {
250
+ ...(room?.template_snapshot
251
+ ? { template: `${room.template_snapshot.name}@${room.template_snapshot.version}` }
252
+ : task.template ? { template: `${task.template.name}@${task.template.version}` } : {}),
253
+ anonymous: storedRoomLaunchPolicy(room?.room_policy ?? task.execution_plan?.room_policy).anonymous,
254
+ owner_attached: Boolean(room?.owner_seat_cid),
255
+ },
256
+ members: { expected, active, launched },
257
+ ...(blocker ? { blocker } : {}),
258
+ ...(nextAction ? { next_action: nextAction } : {}),
259
+ };
260
+ }
261
+ async awaitTaskProvisioning(input) {
262
+ const now = this.deps.now ?? Date.now;
263
+ const sleep = this.deps.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
264
+ const deadline = now() + (input.waitMs ?? 60_000);
265
+ for (;;) {
266
+ const outcome = this.taskProvisioningOutcome(input.taskId);
267
+ if (outcome.kind !== 'in_progress' || now() >= deadline) {
268
+ recordTaskProvisioningOutcome(outcome);
269
+ return outcome;
270
+ }
271
+ await sleep(100);
272
+ }
273
+ }
218
274
  blockTask(input) {
219
275
  return persistBlockTask(input.taskId, input.reason);
220
276
  }
@@ -224,8 +280,93 @@ export class TaskRoomApplicationService {
224
280
  reviewTask(input) {
225
281
  return persistReviewTask(input.taskId);
226
282
  }
227
- deleteTask(input) {
228
- return persistDeleteTask(input.taskId);
283
+ deletionActor(actor) {
284
+ if (actor.kind === 'local_control')
285
+ return { kind: 'local_control', surface: actor.surface };
286
+ if (actor.kind === 'authenticated_owner')
287
+ return { kind: 'authenticated_owner', surface: 'messenger', cid: actor.cid };
288
+ throw new Error('internal workers cannot originate a task deletion acceptance');
289
+ }
290
+ /** Accept a permanent deletion in any lifecycle state; audit at acceptance. */
291
+ async requestTaskDeletion(input) {
292
+ const result = await acceptTaskDeletion(input.taskId, this.deletionActor(input.actor));
293
+ if (result.status === 'accepted') {
294
+ const task = result.task;
295
+ recordFleetAuditPresentation({ kind: 'task', operation: 'delete', id: task.task_id,
296
+ title: task.title, previousState: task.state, newState: 'deleting',
297
+ revision: task.deletion?.accepted_at ?? task.created_at, list: task.list_name ?? 'default',
298
+ roomId: task.room_id,
299
+ template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
300
+ agents: [] });
301
+ }
302
+ return result;
303
+ }
304
+ /** Worker entry: converge an accepted deletion; Cowork is resolved lazily. */
305
+ async settleTaskDeletion(input) {
306
+ const result = await settleTaskDeletion({
307
+ taskId: input.taskId,
308
+ cowork: () => {
309
+ // Config resolves only when room work exists: a no-room deletion must
310
+ // settle even with missing or invalid rooms configuration.
311
+ const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
312
+ if (!cfg.rooms)
313
+ throw new ConfigError('rooms: configuration is required before deleting task rooms');
314
+ return this.deps.cowork ? this.deps.cowork(cfg)
315
+ : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
316
+ },
317
+ });
318
+ if (result.deleted && result.previous_state) {
319
+ recordFleetAuditPresentation({ kind: 'task', operation: 'delete', id: result.task_id,
320
+ title: result.title, previousState: result.previous_state, newState: 'deleted',
321
+ revision: new Date().toISOString(), list: 'default', roomId: undefined,
322
+ template: undefined, agents: [] });
323
+ }
324
+ return result;
325
+ }
326
+ /**
327
+ * Launch the external deletion worker outside the caller's lifecycle and
328
+ * wait boundedly for physical absence. Timeouts and launch failures report
329
+ * a pending, recoverable state — never success. A concurrent worker that
330
+ * already removed the record reads as settled.
331
+ */
332
+ async launchTaskDeletionWorker(input) {
333
+ const launch = this.deps.launchDeletionWorker
334
+ ?? ((taskId) => launchFleetWorker(['task', '_settle_delete', taskId], `task-delete-${taskId}`, this.configurationPath));
335
+ const errorAtBefore = (() => {
336
+ try {
337
+ return getDeletingTask(input.taskId).deletion?.error_at;
338
+ }
339
+ catch {
340
+ return undefined;
341
+ }
342
+ })();
343
+ try {
344
+ await launch(input.taskId);
345
+ }
346
+ catch (error) {
347
+ const message = error instanceof Error ? error.message : String(error);
348
+ await recordTaskDeletionError(input.taskId, message, `External delete worker failed to start. Repeat the delete request for ${input.taskId}.`).catch(() => { });
349
+ return { deleted: false, pending: true, error: message };
350
+ }
351
+ const now = this.deps.now ?? Date.now;
352
+ const sleep = this.deps.sleep
353
+ ?? ((ms) => new Promise(resolve => { setTimeout(resolve, ms); }));
354
+ const deadline = now() + (input.waitMs ?? 10_000);
355
+ while (now() < deadline) {
356
+ if (taskDeletionState(input.taskId) === 'absent')
357
+ return { deleted: true, pending: false };
358
+ try {
359
+ const current = getDeletingTask(input.taskId).deletion;
360
+ if (current?.error_at !== errorAtBefore && current?.error)
361
+ return { deleted: false, pending: true, error: current.error };
362
+ }
363
+ catch { /* re-checked as absence on the next iteration */ }
364
+ await sleep(100);
365
+ }
366
+ return { deleted: false, pending: true };
367
+ }
368
+ recordDeletionError(input) {
369
+ return recordTaskDeletionError(input.taskId, input.error, input.recoveryHint);
229
370
  }
230
371
  async completeTask(input) {
231
372
  const task = readTask(input.taskId);
@@ -263,33 +404,63 @@ export class TaskRoomApplicationService {
263
404
  recordSettlementError(input) {
264
405
  return recordTaskTerminalIntentError(input.taskId, input.error, input.recoveryHint);
265
406
  }
266
- async beginTaskRecovery(input) {
267
- const config = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
268
- this.recovery = { taskId: input.taskId, config };
269
- const task = readTask(input.taskId);
270
- if (task.terminal_intent?.status === 'pending')
271
- return { kind: 'terminal_worker_required', taskId: input.taskId };
272
- return { kind: 'final', result: await this.continueTaskRecovery({
273
- actor: input.actor, taskId: input.taskId, terminalTimedOut: false,
274
- }) };
275
- }
276
- async continueTaskRecovery(input) {
277
- if (!this.recovery || this.recovery.taskId !== input.taskId)
278
- throw new Error('task recovery continuation requires a matching begin');
279
- const recovery = this.recovery;
280
- this.recovery = undefined;
281
- const cfg = recovery.config;
407
+ async continueTaskProvisioning(input) {
408
+ const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
282
409
  let task = readTask(input.taskId);
283
410
  let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
284
- const issues = input.terminalTimedOut ? [{ code: 'terminal_pending' }] : [];
411
+ const issues = [];
285
412
  if (task.state !== 'provisioning')
286
413
  return {
287
- kind: TASK_TERMINAL_STATES.includes(task.state) ? 'terminal' : 'no_op', task, room, issues,
414
+ kind: 'no_op', task, room, issues,
288
415
  };
416
+ if (!room) {
417
+ if (!task.template)
418
+ return {
419
+ kind: 'provisioning_non_resumable', task, room, issues, reason: 'missing_room',
420
+ };
421
+ try {
422
+ const template = task.execution_plan?.snapshot ?? this.existingTemplate(cfg, task);
423
+ const roomPolicy = task.execution_plan
424
+ ? storedRoomLaunchPolicy(task.execution_plan.room_policy)
425
+ : resolveRoomLaunchPolicy(template, undefined);
426
+ await this.provisionRoom(cfg, task, template, created => {
427
+ task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
428
+ }, undefined, roomPolicy);
429
+ task = readTask(task.task_id);
430
+ room = task.room_id ? getRoomRecord(task.room_id) : undefined;
431
+ return { kind: 'provisioning_resumed', task, room, issues: [{ code: 'provisioning_resumed' }] };
432
+ }
433
+ catch (error) {
434
+ issues.push({ code: 'resume_failed', error: error instanceof Error ? error.message : String(error) });
435
+ return { kind: 'provisioning_resume_failed', task, room, issues };
436
+ }
437
+ }
438
+ if (room.saga.phase === 'attach_owner'
439
+ || room.provisioning_detail === 'waiting_owner_authorization'
440
+ || room.provisioning_detail === 'waiting_owner_invite'
441
+ || room.provisioning_detail === 'owner_cid_mismatch') {
442
+ try {
443
+ await this.reconcileProvisioningOwner(cfg, room.room_id);
444
+ task = readTask(task.task_id);
445
+ room = getRoomRecord(room.room_id);
446
+ if (task.state === 'active' && room?.state === 'active')
447
+ return {
448
+ kind: 'provisioning_resumed', task, room, issues: [{ code: 'provisioning_resumed' }],
449
+ };
450
+ }
451
+ catch (error) {
452
+ issues.push({ code: 'resume_failed', error: error instanceof Error ? error.message : String(error) });
453
+ return { kind: 'provisioning_resume_failed', task, room, issues };
454
+ }
455
+ }
289
456
  if (!room)
290
- return { kind: 'provisioning_non_resumable', task, room, issues, reason: 'missing_room' };
457
+ return {
458
+ kind: 'provisioning_non_resumable', task, room, issues, reason: 'missing_room',
459
+ };
291
460
  if (room.provisioning_detail === 'waiting_cowork')
292
461
  issues.push({ code: 'waiting_cowork' });
462
+ if (room.provisioning_detail === 'waiting_owner_authorization')
463
+ issues.push({ code: 'waiting_owner_authorization' });
293
464
  if (room.provisioning_detail === 'waiting_owner_invite')
294
465
  issues.push({ code: 'waiting_owner_invite' });
295
466
  if (room.provisioning_detail === 'owner_cid_mismatch')
@@ -326,6 +497,50 @@ export class TaskRoomApplicationService {
326
497
  return { kind: 'provisioning_resume_failed', task, room, issues };
327
498
  }
328
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
+ }
329
544
  async acceptTerminal(taskId, kind, roomId, outcome) {
330
545
  const task = await acceptTaskTerminalIntent({ taskId, kind, roomId, outcome });
331
546
  return { task, settlementRequired: task.terminal_intent?.status === 'pending' && !!roomId };
@@ -345,11 +560,9 @@ export class TaskRoomApplicationService {
345
560
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
346
561
  return listTemplates(cfg.roomTemplates ?? {}).flatMap(template => {
347
562
  const issues = template.members.flatMap(member => {
348
- if (!('ref' in member.agent))
349
- return [];
350
- const ref = member.agent.ref;
351
- return cfg.roles.some(role => role.name === ref)
352
- ? [] : [`member ${member.slot}: Agent ref '${ref}' not found`];
563
+ const ref = member.agent_template;
564
+ return cfg.agentTemplates?.[ref]
565
+ ? [] : [`member ${member.slot}: Agent Template '${ref}' not found`];
353
566
  });
354
567
  return issues.length ? [{ template: `${template.name}@${template.version}`, issues }] : [];
355
568
  });
@@ -420,73 +633,6 @@ export class TaskRoomApplicationService {
420
633
  recordRoomSettlementError(input) {
421
634
  return recordManagedRoomCloseError(input.roomId, input.error, input.recoveryHint);
422
635
  }
423
- async recoverRoom(input) {
424
- const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
425
- if (!cfg.rooms)
426
- throw new ConfigError('rooms: configuration is required before creating or querying rooms');
427
- const adapter = this.deps.cowork ? this.deps.cowork(cfg)
428
- : createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
429
- let orchestration = getRoomRecord(input.roomId);
430
- if (orchestration?.state === 'closing' || orchestration?.state === 'closed')
431
- return { kind: 'deletion_worker_required', roomId: input.roomId };
432
- const room = await adapter.recoverRoom(input.roomId);
433
- orchestration = getRoomRecord(input.roomId);
434
- if (orchestration && !orchestration.owner_seat_cid
435
- && (orchestration.provisioning_detail === 'waiting_owner_invite'
436
- || orchestration.provisioning_detail === 'owner_cid_mismatch')) {
437
- const expected = cfg.rooms.owner.expected_cid.toLowerCase();
438
- const existing = (await adapter.getSeats(input.roomId))
439
- .find(seat => seat.identity_cid.toLowerCase() === expected && seat.seat_state !== 'removed');
440
- if (!existing && !cfg.ownerInvite)
441
- throw new ConfigError('rooms.owner: configure public_invite or public_invite_file before recovery');
442
- let acceptedCid = existing?.identity_cid;
443
- if (!acceptedCid)
444
- acceptedCid = (await adapter.acceptInvite(input.roomId, cfg.ownerInvite, {
445
- role: cfg.rooms.owner.role, expected_cid: cfg.rooms.owner.expected_cid,
446
- })).seat_cid;
447
- setOwnerSeat(input.roomId, acceptedCid, cfg.ownerInviteFingerprint ?? '');
448
- orchestration = advanceSaga(input.roomId, 'create_members', 3);
449
- }
450
- const issues = [];
451
- if (orchestration?.saga.error)
452
- issues.push('A provisioning failure is recorded; inspect role logs for diagnostics.');
453
- if (orchestration?.saga.recovery_hint)
454
- issues.push('Recovery guidance is recorded; inspect role logs for diagnostics.');
455
- if (orchestration?.provisioning_detail === 'waiting_cowork')
456
- issues.push('Check ours-cowork service status');
457
- if (orchestration?.provisioning_detail === 'waiting_owner_invite')
458
- issues.push('Rotate rooms.owner.public_invite in config, then re-run recover');
459
- if (orchestration?.provisioning_detail === 'waiting_seats')
460
- issues.push('Inspect temporary member logs for invite acceptance, then re-run recover');
461
- if (orchestration?.state === 'provisioning'
462
- && ['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(orchestration.saga.phase)
463
- && orchestration.template_snapshot) {
464
- try {
465
- let template = orchestration.template_snapshot;
466
- let brief;
467
- let goal = orchestration.room_name;
468
- if (orchestration.task_id) {
469
- const task = readTask(orchestration.task_id);
470
- if (!task.template || task.template.name !== template.name || task.template.version !== template.version
471
- || task.template.content_hash !== template.content_hash)
472
- throw new Error(`task ${task.task_id} template reference does not match room ${orchestration.room_id}'s durable snapshot`);
473
- brief = task.brief;
474
- goal = task.title;
475
- }
476
- await (this.deps.provisionMembers ?? provisionMembers)({ cfg, cowork: adapter,
477
- roomId: orchestration.room_id, taskId: orchestration.task_id, template,
478
- binPath: (this.deps.binPath ?? getBinPath)(), brief, goal });
479
- orchestration = getRoomRecord(input.roomId);
480
- return { kind: 'provisioning_resumed', room, orchestration,
481
- issues: ['Provisioning resumed successfully'] };
482
- }
483
- catch (error) {
484
- issues.push(`Resume failed: ${error instanceof Error ? error.message : String(error)}`);
485
- return { kind: 'provisioning_resume_failed', room, orchestration, issues };
486
- }
487
- }
488
- return { kind: 'recovered', room, orchestration, issues };
489
- }
490
636
  async finishTask(input) {
491
637
  let task = readTask(input.taskId);
492
638
  if (TASK_TERMINAL_STATES.includes(task.state))
@@ -500,81 +646,161 @@ export class TaskRoomApplicationService {
500
646
  async ensureTaskWork(input) {
501
647
  const cfg = (this.deps.loadConfiguration ?? loadConfig)(this.configurationPath);
502
648
  let task = readTask(input.taskId);
649
+ const recordedRoom = task.room_id ? getRoomRecord(task.room_id) : undefined;
650
+ const recordedPolicy = storedRoomLaunchPolicy(recordedRoom?.room_policy ?? task.execution_plan?.room_policy);
651
+ if (task.room_id && input.anonymous !== undefined && input.anonymous !== recordedPolicy.anonymous)
652
+ throw new TaskRoomApplicationError('template_mismatch', 'anonymous override does not match the existing Room launch policy', { room: task.room_id });
503
653
  if (TASK_TERMINAL_STATES.includes(task.state))
504
654
  throw new TaskRoomApplicationError('task_terminal', 'task terminal', { task: input.taskId, state: task.state });
655
+ const durableTemplate = task.room_id
656
+ ? getRoomRecord(task.room_id)?.template_snapshot ?? task.execution_plan?.snapshot
657
+ : task.execution_plan?.snapshot;
658
+ const templateRequestMatches = !input.template || (durableTemplate !== undefined
659
+ && (input.template === durableTemplate.name
660
+ || input.template === `${durableTemplate.name}@${durableTemplate.version}`));
661
+ const memberRequestMatches = input.members === undefined || (task.execution_plan !== undefined
662
+ && task.execution_plan.overrides_hash === hashMemberOverrides(input.members));
663
+ const durableRequestMatches = durableTemplate !== undefined
664
+ && templateRequestMatches && memberRequestMatches;
505
665
  if (task.state === 'active' && task.room_id) {
506
- if (input.template) {
507
- const definition = resolveTemplate(input.template, cfg.roomTemplates ?? {});
666
+ if (input.template || input.members) {
667
+ const pinned = durableTemplate;
668
+ if (!pinned)
669
+ throw new TaskRoomApplicationError('template_mismatch', 'active task has no durable execution plan', { room: task.room_id });
670
+ if (durableRequestMatches)
671
+ return { task, status: 'already_active' };
672
+ const templateName = input.template ?? pinned.name;
673
+ const definition = resolveTemplate(templateName, cfg.roomTemplates ?? {});
508
674
  if (!definition)
509
- throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: input.template });
510
- const requested = snapshotTemplate(definition);
511
- const pinned = getRoomRecord(task.room_id)?.template_snapshot ?? task.template;
512
- if (pinned && (pinned.name !== requested.name || pinned.content_hash !== requested.content_hash))
675
+ throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
676
+ const requested = prepareExecutionPlan(definition, cfg, input.members ?? {});
677
+ const pinnedPlanHash = task.execution_plan?.plan_hash ?? pinned.content_hash;
678
+ if (pinned.name !== requested.snapshot.name || pinnedPlanHash !== requested.planHash)
513
679
  throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
514
- requested: `${requested.name}@${requested.version}`, room: task.room_id,
680
+ requested: `${requested.snapshot.name}@${requested.snapshot.version}`, room: task.room_id,
515
681
  provisioned: `${pinned.name}@${pinned.version}`
516
682
  });
517
683
  }
518
684
  return { task, status: 'already_active' };
519
685
  }
520
- const room = task.room_id ? getRoomRecord(task.room_id) : undefined;
521
- const durable = room?.template_snapshot;
686
+ let room = task.room_id ? getRoomRecord(task.room_id) : undefined;
687
+ const durable = room?.template_snapshot ?? task.execution_plan?.snapshot;
522
688
  if (durable && (!task.template || task.template.name !== durable.name
523
689
  || task.template.version !== durable.version || task.template.content_hash !== durable.content_hash))
524
- throw new Error(`task ${task.task_id} template reference does not match room ${room.room_id}'s durable snapshot`);
690
+ throw new Error(`task ${task.task_id} template reference does not match its durable execution snapshot`);
525
691
  const templateName = input.template ?? durable?.name ?? task.template?.name
526
692
  ?? cfg.tasks?.default_room_template ?? cfg.rooms?.defaults?.template ?? 'single';
527
- const definition = durable && !input.template ? undefined
693
+ const storedOverridesMatch = !!(durable && memberRequestMatches);
694
+ const definition = durable && durableRequestMatches ? undefined
528
695
  : resolveTemplate(templateName, cfg.roomTemplates ?? {});
529
- if (input.template && !definition)
696
+ if (input.template && !definition && !durableRequestMatches)
530
697
  throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
531
698
  if (!durable && !definition)
532
699
  throw new TaskRoomApplicationError('template_not_found', 'template not found', { template: templateName });
533
- const snapshot = durable ?? snapshotTemplate(definition);
700
+ let launchDefinitions;
701
+ let preparedPlan = !durable && definition
702
+ ? prepareExecutionPlan(definition, cfg, input.members ?? {}) : undefined;
703
+ let snapshot = durable ?? preparedPlan.snapshot;
704
+ const roomPolicy = input.anonymous !== undefined
705
+ ? resolveRoomLaunchPolicy(snapshot, input.anonymous)
706
+ : task.execution_plan
707
+ ? storedRoomLaunchPolicy(task.execution_plan.room_policy)
708
+ : resolveRoomLaunchPolicy(snapshot, undefined);
709
+ if (preparedPlan)
710
+ launchDefinitions = preparedPlan.launchDefinitions;
711
+ if (input.members && Object.keys(input.members).length && !storedOverridesMatch) {
712
+ if (!definition)
713
+ throw new TaskRoomApplicationError('template_mismatch', 'member overrides cannot change an existing Room execution plan');
714
+ const prepared = preparedPlan ?? prepareExecutionPlan(definition, cfg, input.members);
715
+ preparedPlan = prepared;
716
+ if (durable && prepared.planHash !== durable.content_hash)
717
+ throw new TaskRoomApplicationError('template_mismatch', 'member overrides do not match existing execution plan');
718
+ snapshot = prepared.snapshot;
719
+ launchDefinitions = prepared.launchDefinitions;
720
+ }
534
721
  if (durable && input.template && definition) {
535
- const requested = snapshotTemplate(definition);
722
+ const requested = snapshotTemplate(definition, cfg.agentTemplates);
536
723
  if (requested.name !== durable.name || requested.content_hash !== durable.content_hash)
537
724
  throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
538
725
  requested: `${requested.name}@${requested.version}`, room: room.room_id,
539
726
  provisioned: `${durable.name}@${durable.version}`
540
727
  });
541
728
  }
542
- if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash)
543
- throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
544
- template: `${task.template.name}@${task.template.version}`
545
- });
729
+ if (!input.template && task.template && snapshot.content_hash !== task.template.content_hash) {
730
+ // Pre-execution-plan backlog records used the template-only hash. Accept
731
+ // that exact legacy pin once, then upgrade it to the complete sealed plan.
732
+ const legacyHash = definition
733
+ ? snapshotTemplate(definition, cfg.agentTemplates).content_hash : undefined;
734
+ if (task.template.content_hash !== legacyHash)
735
+ throw new TaskRoomApplicationError('task_template_drift', 'task template drift', {
736
+ template: `${task.template.name}@${task.template.version}`
737
+ });
738
+ }
546
739
  if (task.room_id && room?.template_snapshot
547
740
  && (room.template_snapshot.name !== snapshot.name || room.template_snapshot.content_hash !== snapshot.content_hash))
548
741
  throw new TaskRoomApplicationError('template_mismatch', 'template mismatch', {
549
742
  requested: `${snapshot.name}@${snapshot.version}`, room: task.room_id,
550
743
  provisioned: `${room.template_snapshot.name}@${room.template_snapshot.version}`
551
744
  });
552
- if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
745
+ if (preparedPlan && !task.room_id) {
746
+ const unlock = acquireLaunchSnapshotLock();
747
+ let sealed;
748
+ try {
749
+ sealed = sealTemplateSnapshot(snapshot, cfg.agentTemplates ?? {}, launchDefinitions);
750
+ task = updateTaskExecutionPlan(task.task_id, { schema_version: 1, snapshot: sealed,
751
+ room_policy: roomPolicy,
752
+ overrides: preparedPlan.overrides, overrides_hash: preparedPlan.overridesHash,
753
+ plan_hash: preparedPlan.planHash });
754
+ snapshot = sealed;
755
+ }
756
+ catch (error) {
757
+ unlock();
758
+ if (sealed?.launch_snapshot_hash)
759
+ releaseLaunchSnapshot(sealed.launch_snapshot_hash);
760
+ throw error;
761
+ }
762
+ unlock();
763
+ }
764
+ else if (!task.room_id && task.execution_plan && input.anonymous !== undefined) {
765
+ task = updateTaskExecutionPlan(task.task_id, { ...task.execution_plan, room_policy: roomPolicy });
766
+ }
767
+ else if (!task.template || task.template.name !== snapshot.name || task.template.content_hash !== snapshot.content_hash)
553
768
  task = updateTaskTemplate(task.task_id, { name: snapshot.name, version: snapshot.version, content_hash: snapshot.content_hash });
554
769
  if (task.state === 'backlog') {
555
770
  task = transitionTask(task.task_id);
556
- recordFleetAuditPresentation({ kind: 'task', operation: 'start', id: task.task_id,
557
- title: task.title, previousState: 'backlog', newState: task.state,
558
- revision: task.started_at ?? task.created_at, list: task.list_name ?? 'default',
559
- template: `${snapshot.name}@${snapshot.version}`, agents: [] });
560
771
  }
561
772
  if (!task.room_id) {
562
773
  try {
563
774
  await this.provisionRoom(cfg, task, snapshot, created => {
564
775
  task = updateTaskRoom(task.task_id, created.room_id, created.room_identity_cid);
565
- });
776
+ }, launchDefinitions, roomPolicy);
566
777
  task = readTask(task.task_id);
567
778
  }
568
779
  catch (error) {
569
780
  if (error instanceof CoworkUnavailableError)
570
781
  persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
571
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
572
- id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
573
- eventId: task.room_id ? roomFailureEventId(getRoomRecord(task.room_id)) : task.created_at });
574
- throw error;
782
+ const current = readTask(task.task_id);
783
+ if (!current.room_id || !getRoomRecord(current.room_id))
784
+ throw error;
785
+ task = current;
575
786
  }
576
787
  }
577
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
+ }
578
804
  if (!room || !['create_members', 'join_role_groups', 'wait_seats', 'launch_work', 'activate'].includes(room.saga.phase))
579
805
  throw new TaskRoomApplicationError('task_non_resumable', 'task non-resumable', { task: task.task_id, room: task.room_id });
580
806
  try {
@@ -589,24 +815,11 @@ export class TaskRoomApplicationService {
589
815
  catch (error) {
590
816
  if (error instanceof CoworkUnavailableError)
591
817
  persistBlockTask(task.task_id, 'Cowork management socket is unavailable');
592
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
593
- id: task.task_id, state: readTask(task.task_id).state, category: 'provision_failed',
594
- eventId: roomFailureEventId(getRoomRecord(task.room_id)) });
595
- throw error;
818
+ task = readTask(task.task_id);
596
819
  }
597
820
  }
598
821
  const resultingRoom = getRoomRecord(task.room_id);
599
- if (task.state === 'active' && resultingRoom?.state === 'active')
600
- recordFleetAuditPresentation({ kind: 'task', operation: 'work', id: task.task_id,
601
- title: task.title, previousState: 'provisioning', newState: task.state,
602
- revision: getRoomRecord(task.room_id)?.activated_at ?? task.started_at ?? task.created_at,
603
- list: task.list_name ?? 'default', roomId: task.room_id,
604
- template: task.template ? `${task.template.name}@${task.template.version}` : undefined,
605
- agents: taskAgents(task) });
606
- else if (resultingRoom)
607
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Task',
608
- id: task.task_id, state: task.state, category: 'provision_pending', eventId: roomFailureEventId(resultingRoom) });
609
- return { task, status: 'ready' };
822
+ return { task, status: task.state === 'active' && resultingRoom?.state === 'active' ? 'ready' : 'in_progress' };
610
823
  }
611
824
  createTemplate(cfg, requested, noRoom) {
612
825
  if (noRoom)
@@ -619,17 +832,17 @@ export class TaskRoomApplicationService {
619
832
  throw new TaskRoomApplicationError('template_not_found', `template not found: ${requested}`);
620
833
  return undefined;
621
834
  }
622
- return snapshotTemplate(definition);
835
+ return snapshotTemplate(definition, cfg.agentTemplates);
623
836
  }
624
837
  existingTemplate(cfg, task) {
625
838
  const ref = task.template;
626
839
  const definition = resolveTemplate(ref.name, cfg.roomTemplates ?? {});
627
- const snapshot = definition ? snapshotTemplate(definition) : undefined;
840
+ const snapshot = definition ? snapshotTemplate(definition, cfg.agentTemplates) : undefined;
628
841
  if (!snapshot || snapshot.content_hash !== ref.content_hash)
629
842
  throw new TaskRoomApplicationError('task_template_drift', `task template snapshot no longer matches ${ref.name}@${ref.version}`);
630
843
  return snapshot;
631
844
  }
632
- async provisionRoom(cfg, task, template, onCreated) {
845
+ async provisionRoom(cfg, task, template, onCreated, launchDefinitions, policy = resolveRoomLaunchPolicy(template, undefined)) {
633
846
  const rooms = cfg.rooms;
634
847
  if (!rooms)
635
848
  throw new ConfigError('rooms: configuration is required');
@@ -641,26 +854,61 @@ export class TaskRoomApplicationService {
641
854
  throw new ConfigError('rooms: configuration is required before creating or querying rooms');
642
855
  return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
643
856
  })();
644
- const created = await cowork.createRoom({
645
- room_name: task.title, goal: task.goal?.trim() || task.title,
646
- briefing: task.brief?.trim() || template?.contract?.trim() || task.goal?.trim() || task.title,
647
- quiet_membership: template?.room?.quiet_membership,
648
- anonymous: template?.room?.anonymous,
649
- });
650
- let room = createRoomRecord({
651
- room_id: created.room_id, room_name: task.title, room_identity_cid: created.identity_cid,
652
- task_id: task.task_id, template_snapshot: template,
653
- });
654
- recordFleetAuditPresentation({ kind: 'room', operation: 'create', id: room.room_id,
655
- name: room.room_name, previousState: 'none', newState: 'provisioning',
656
- revision: room.created_at, taskId: room.task_id,
657
- template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
658
- participants: [] });
659
- onCreated(room);
857
+ const roomName = task.task_id ? deriveTaskRoomName(task.title, task.task_id) : task.title;
858
+ const unlockSnapshot = template ? acquireLaunchSnapshotLock() : undefined;
859
+ let launchTemplate;
860
+ let room;
861
+ try {
862
+ launchTemplate = template ? (template.launch_snapshot_hash ? template
863
+ : sealTemplateSnapshot(template, cfg.agentTemplates ?? {}, launchDefinitions)) : undefined;
864
+ // Room publication window: for a task-bound room, hold the common
865
+ // task-operation lock across remote creation, local record creation, and
866
+ // the task link, so an accepted deletion linearizes strictly before
867
+ // (bounded task_deleting abort) or strictly after (record visible to the
868
+ // deletion scan). Lock order stays launch-snapshot → task-operation.
869
+ const publishRoom = async () => {
870
+ if (task.task_id) {
871
+ const fresh = readTask(task.task_id);
872
+ if (fresh.deletion?.status === 'pending')
873
+ throw new TaskRoomApplicationError('task_deleting', `task ${task.task_id} is pending deletion`, { task: task.task_id });
874
+ }
875
+ const created = await cowork.createRoom({
876
+ room_name: roomName, goal: task.goal?.trim() || task.title,
877
+ briefing: task.brief?.trim() || launchTemplate?.contract?.trim() || task.goal?.trim() || task.title,
878
+ quiet_membership: launchTemplate?.room?.quiet_membership,
879
+ anonymous: policy.anonymous,
880
+ });
881
+ const record = createRoomRecord({
882
+ room_id: created.room_id, room_name: roomName, room_identity_cid: created.identity_cid,
883
+ task_id: task.task_id, template_snapshot: launchTemplate, room_policy: policy,
884
+ });
885
+ onCreated(record);
886
+ return record;
887
+ };
888
+ room = task.task_id
889
+ ? await withFileLock(taskOperationLockPath(task.task_id), publishRoom, {}, TASK_OPERATION_LOCK_STALE_MS)
890
+ : await publishRoom();
891
+ }
892
+ catch (error) {
893
+ unlockSnapshot?.();
894
+ if (launchTemplate?.launch_snapshot_hash)
895
+ releaseLaunchSnapshot(launchTemplate.launch_snapshot_hash);
896
+ throw error;
897
+ }
898
+ unlockSnapshot?.();
660
899
  room = advanceSaga(room.room_id, 'create_room', 1);
661
900
  if (attachOwner) {
901
+ room = advanceSaga(room.room_id, 'attach_owner', 2);
902
+ try {
903
+ await cowork.setRoleCommands(room.room_id, {
904
+ role: rooms.owner.role, commands: [...OWNER_ROOM_COMMANDS],
905
+ });
906
+ }
907
+ catch (error) {
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');
909
+ throw error;
910
+ }
662
911
  try {
663
- room = advanceSaga(room.room_id, 'attach_owner', 2);
664
912
  const accepted = await cowork.acceptInvite(room.room_id, cfg.ownerInvite, {
665
913
  role: rooms.owner.role, expected_cid: rooms.owner.expected_cid,
666
914
  });
@@ -669,40 +917,65 @@ export class TaskRoomApplicationService {
669
917
  catch (error) {
670
918
  const mismatch = error instanceof CoworkProtocolError && /CID|expected/i.test(error.message);
671
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.'
672
- : 'Rotate rooms.owner.public_invite or public_invite_file, then run room recover.', mismatch ? 'owner_cid_mismatch' : 'waiting_owner_invite');
673
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room',
674
- id: room.room_id, state: getRoomRecord(room.room_id)?.state ?? 'provisioning',
675
- category: 'provision_failed', eventId: roomFailureEventId(getRoomRecord(room.room_id) ?? room) });
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 });
676
925
  throw error;
677
926
  }
678
927
  }
679
928
  room = advanceSaga(room.room_id, 'create_members', 3);
680
- if (template?.members.length)
929
+ if (launchTemplate && (launchTemplate.members.length > 0 || attachOwner))
681
930
  try {
682
931
  room = await (this.deps.provisionMembers ?? provisionMembers)({
683
- cfg, cowork, roomId: room.room_id, taskId: task.task_id, template,
932
+ cfg, cowork, roomId: room.room_id, taskId: task.task_id, template: launchTemplate,
684
933
  binPath: (this.deps.binPath ?? getBinPath)(), brief: task.brief,
685
934
  goal: task.task_id ? task.title : task.goal,
686
935
  });
687
936
  }
688
937
  catch (error) {
689
- const failed = getRoomRecord(room.room_id) ?? room;
690
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: failed.room_id,
691
- state: failed.state, category: 'provision_failed', eventId: roomFailureEventId(failed) });
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 });
692
942
  throw error;
693
943
  }
944
+ else if (attachOwner) {
945
+ const now = this.deps.now ?? Date.now;
946
+ const sleep = this.deps.sleep ?? (ms => new Promise(resolve => setTimeout(resolve, ms)));
947
+ const deadline = now() + 60_000;
948
+ for (;;) {
949
+ const seats = await cowork.getSeats(room.room_id);
950
+ if (room.owner_seat_cid && seats.some(seat => seat.identity_cid.toLowerCase() === room.owner_seat_cid.toLowerCase()
951
+ && seat.seat_state === 'active')) {
952
+ room = activateRoom(room.room_id);
953
+ break;
954
+ }
955
+ if (now() >= deadline) {
956
+ room = advanceSaga(room.room_id, 'wait_seats', 5, 'waiting_seats');
957
+ break;
958
+ }
959
+ await sleep(250);
960
+ }
961
+ }
694
962
  else
695
963
  room = activateRoom(room.room_id);
696
- if (room.state === 'active')
697
- recordFleetAuditPresentation({ kind: 'room', operation: 'activate', id: room.room_id,
698
- name: room.room_name, previousState: 'provisioning', newState: 'active',
699
- 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,
700
969
  template: room.template_snapshot ? `${room.template_snapshot.name}@${room.template_snapshot.version}` : undefined,
701
- participants: roomParticipants(room) });
702
- else
703
- recordFleetAuditPresentation({ kind: 'lifecycle_failure', resource: 'Room', id: room.room_id,
704
- state: room.state, category: 'provision_pending', eventId: roomFailureEventId(room) });
705
- if (task.task_id && !template?.members.length)
970
+ anonymous: policy.anonymous, memberCount: room.member_seats.length, ownerAttached: attachOwner,
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
+ });
978
+ if (task.task_id && !launchTemplate?.members.length)
706
979
  activateTask(task.task_id);
707
980
  return room;
708
981
  }