@ours.network/fleet 1.2.0-nightly.2 → 1.2.0-nightly.21

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 (277) hide show
  1. package/README.md +152 -29
  2. package/dist/agent-ours/bridge.d.ts +3 -0
  3. package/dist/agent-ours/bridge.js +178 -0
  4. package/dist/agent-ours/controller.d.ts +14 -0
  5. package/dist/agent-ours/controller.js +60 -0
  6. package/dist/agent-ours/correspondence.d.ts +10 -0
  7. package/dist/agent-ours/correspondence.js +1 -0
  8. package/dist/agent-ours/file-rpc.d.ts +23 -0
  9. package/dist/agent-ours/file-rpc.js +123 -0
  10. package/dist/agent-ours/harness.d.ts +14 -0
  11. package/dist/agent-ours/harness.js +72 -0
  12. package/dist/agent-ours/mcp-endpoint.d.ts +15 -0
  13. package/dist/agent-ours/mcp-endpoint.js +111 -0
  14. package/dist/agent-ours/runtime.d.ts +59 -0
  15. package/dist/agent-ours/runtime.js +283 -0
  16. package/dist/agent-ours/service.d.ts +23 -0
  17. package/dist/agent-ours/service.js +372 -0
  18. package/dist/agent-ours/state.d.ts +45 -0
  19. package/dist/agent-ours/state.js +95 -0
  20. package/dist/agent-ours/wire.d.ts +14 -0
  21. package/dist/agent-ours/wire.js +55 -0
  22. package/dist/agent-recovery-gate.js +1 -1
  23. package/dist/application/conversation-history.d.ts +6 -0
  24. package/dist/application/conversation-history.js +65 -0
  25. package/dist/application/fleet-query-service.d.ts +1 -1
  26. package/dist/application/fleet-query-service.js +2 -2
  27. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  28. package/dist/application/legacy-supervisor-identity.js +43 -0
  29. package/dist/application/model-catalog.d.ts +1 -1
  30. package/dist/application/model-catalog.js +5 -2
  31. package/dist/application/preset-provenance.d.ts +19 -0
  32. package/dist/application/preset-provenance.js +59 -0
  33. package/dist/application/room-layout-definitions.d.ts +45 -0
  34. package/dist/application/room-layout-definitions.js +130 -0
  35. package/dist/application/session-control.d.ts +8 -0
  36. package/dist/application/session-control.js +21 -3
  37. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  38. package/dist/application/supervisor-ours-tools.js +113 -0
  39. package/dist/application/task-layouts.d.ts +89 -0
  40. package/dist/application/task-layouts.js +248 -0
  41. package/dist/application/task-room-service.d.ts +99 -3
  42. package/dist/application/task-room-service.js +296 -31
  43. package/dist/atomic-file.d.ts +5 -0
  44. package/dist/atomic-file.js +62 -1
  45. package/dist/briefing.js +10 -59
  46. package/dist/build-info.json +5 -4
  47. package/dist/capabilities.d.ts +2 -1
  48. package/dist/capabilities.js +2 -0
  49. package/dist/cli.js +65 -2
  50. package/dist/client-profile.d.ts +5 -16
  51. package/dist/client-profile.js +4 -112
  52. package/dist/config.d.ts +4 -0
  53. package/dist/config.js +7 -0
  54. package/dist/creation.d.ts +1 -1
  55. package/dist/creation.js +5 -16
  56. package/dist/daemon-recovery.d.ts +1 -8
  57. package/dist/daemon-recovery.js +8 -113
  58. package/dist/docs.d.ts +3 -3
  59. package/dist/docs.js +43 -23
  60. package/dist/doctor.d.ts +1 -1
  61. package/dist/doctor.js +46 -59
  62. package/dist/erased-resources.d.ts +9 -0
  63. package/dist/erased-resources.js +87 -0
  64. package/dist/fleet-command-audit.d.ts +1 -0
  65. package/dist/fleet-command-audit.js +12 -7
  66. package/dist/harness/agent-session.d.ts +2 -0
  67. package/dist/harness/claude-code-session.js +4 -2
  68. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  69. package/dist/harness/codex-app-server-proxy.js +41 -5
  70. package/dist/harness/codex-session.js +9 -2
  71. package/dist/harness/codex.d.ts +3 -2
  72. package/dist/harness/codex.js +4 -3
  73. package/dist/harness/hermes-session.js +7 -1
  74. package/dist/init-wizard.js +20 -9
  75. package/dist/monitor.d.ts +1 -34
  76. package/dist/monitor.js +24 -151
  77. package/dist/owner-channel/channel.d.ts +3 -0
  78. package/dist/owner-channel/channel.js +34 -6
  79. package/dist/owner-channel/commands.d.ts +4 -0
  80. package/dist/owner-channel/commands.js +8 -0
  81. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  82. package/dist/owner-channel/ours-client.js +2 -17
  83. package/dist/preset-migration.js +15 -0
  84. package/dist/rooms-tasks/add-member.d.ts +48 -0
  85. package/dist/rooms-tasks/add-member.js +83 -0
  86. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  87. package/dist/rooms-tasks/archived-absence.js +48 -0
  88. package/dist/rooms-tasks/cli.js +34 -5
  89. package/dist/rooms-tasks/close.d.ts +5 -2
  90. package/dist/rooms-tasks/close.js +208 -28
  91. package/dist/rooms-tasks/config.js +3 -1
  92. package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
  93. package/dist/rooms-tasks/cowork-adapter.js +11 -117
  94. package/dist/rooms-tasks/cowork-http.d.ts +3 -0
  95. package/dist/rooms-tasks/cowork-http.js +70 -0
  96. package/dist/rooms-tasks/deletion.d.ts +1 -1
  97. package/dist/rooms-tasks/deletion.js +74 -5
  98. package/dist/rooms-tasks/erasure.d.ts +6 -0
  99. package/dist/rooms-tasks/erasure.js +169 -0
  100. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  101. package/dist/rooms-tasks/external-worker.js +3 -4
  102. package/dist/rooms-tasks/layout-cli.d.ts +2 -0
  103. package/dist/rooms-tasks/layout-cli.js +81 -0
  104. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  105. package/dist/rooms-tasks/layout-config.js +89 -0
  106. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  107. package/dist/rooms-tasks/layout-control.js +52 -0
  108. package/dist/rooms-tasks/layout-service.d.ts +39 -0
  109. package/dist/rooms-tasks/layout-service.js +173 -0
  110. package/dist/rooms-tasks/layout.d.ts +102 -0
  111. package/dist/rooms-tasks/layout.js +227 -0
  112. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  113. package/dist/rooms-tasks/live-readiness.js +85 -0
  114. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  115. package/dist/rooms-tasks/member-startup.js +2 -0
  116. package/dist/rooms-tasks/provision.d.ts +14 -0
  117. package/dist/rooms-tasks/provision.js +182 -48
  118. package/dist/rooms-tasks/room-state.d.ts +4 -2
  119. package/dist/rooms-tasks/room-state.js +53 -8
  120. package/dist/rooms-tasks/task-state.d.ts +42 -7
  121. package/dist/rooms-tasks/task-state.js +153 -23
  122. package/dist/rooms-tasks/types.d.ts +28 -0
  123. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  124. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  125. package/dist/rooms-tasks/workspace.d.ts +26 -0
  126. package/dist/rooms-tasks/workspace.js +195 -0
  127. package/dist/runner.d.ts +23 -3
  128. package/dist/runner.js +574 -494
  129. package/dist/session/acp.d.ts +3 -0
  130. package/dist/session/acp.js +7 -0
  131. package/dist/session/arbiter.d.ts +3 -0
  132. package/dist/session/arbiter.js +20 -2
  133. package/dist/session/codex-app-server-transport.js +1 -1
  134. package/dist/session/codex-app-server.d.ts +3 -0
  135. package/dist/session/codex-app-server.js +25 -1
  136. package/dist/session/control.d.ts +10 -1
  137. package/dist/session/control.js +36 -2
  138. package/dist/session/conversation-normalizer.d.ts +2 -1
  139. package/dist/session/conversation-normalizer.js +1 -1
  140. package/dist/session/conversation-types.d.ts +5 -3
  141. package/dist/session/task-notice.d.ts +14 -0
  142. package/dist/session/task-notice.js +13 -0
  143. package/dist/session/types.d.ts +3 -0
  144. package/dist/socket-path.d.ts +2 -0
  145. package/dist/socket-path.js +22 -0
  146. package/dist/spawn.js +19 -16
  147. package/dist/startup-readiness.d.ts +13 -0
  148. package/dist/startup-readiness.js +38 -0
  149. package/dist/supervisor/launchd.js +2 -1
  150. package/dist/supervisor/systemd.js +9 -23
  151. package/dist/temp-idle.d.ts +17 -0
  152. package/dist/temp-idle.js +42 -0
  153. package/dist/temp-lifecycle.d.ts +6 -1
  154. package/dist/temp-lifecycle.js +29 -13
  155. package/dist/temp-supervisor-recovery.d.ts +3 -0
  156. package/dist/temp-supervisor-recovery.js +46 -0
  157. package/dist/watchdog/briefing.js +3 -16
  158. package/dist/watchdog/run.js +19 -5
  159. package/dist/web/agent-attachments.d.ts +29 -0
  160. package/dist/web/agent-attachments.js +162 -0
  161. package/dist/web/auth.d.ts +4 -1
  162. package/dist/web/auth.js +23 -2
  163. package/dist/web/control.js +2 -1
  164. package/dist/web/fleet-config-service.d.ts +4 -1
  165. package/dist/web/fleet-config-service.js +46 -15
  166. package/dist/web/prefix-gateway.d.ts +20 -0
  167. package/dist/web/prefix-gateway.js +150 -0
  168. package/dist/web/runtime.d.ts +4 -0
  169. package/dist/web/runtime.js +19 -14
  170. package/dist/web/server.d.ts +7 -0
  171. package/dist/web/server.js +293 -25
  172. package/docs/room-layouts.md +92 -0
  173. package/docs/task-workspaces.md +130 -0
  174. package/examples/room-layouts/fresh.yaml +27 -0
  175. package/examples/room-layouts/shared.yaml +19 -0
  176. package/package.json +17 -13
  177. package/presets/README.md +78 -0
  178. package/presets/brain-catalog.json +8 -2
  179. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  180. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  181. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  182. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  183. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  184. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  185. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  186. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  187. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  188. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  189. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  190. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  191. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  192. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  193. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  194. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  195. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  196. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  197. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  198. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  199. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  200. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  201. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  202. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  203. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  204. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  205. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  206. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  207. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  208. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  209. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  210. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  211. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  212. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  213. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  214. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  215. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  216. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  217. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  218. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  219. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  220. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  221. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  222. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  223. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  224. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  225. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  226. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  227. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  228. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  229. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  230. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  231. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  232. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  233. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  234. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  235. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  236. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  237. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  238. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  239. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  240. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  241. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  242. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  243. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  244. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  245. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  246. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  247. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  248. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  249. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  250. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  251. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  252. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  253. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  254. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  255. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  256. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  257. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  258. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  259. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  260. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  261. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  262. package/presets/fleet/roles/Coordinator.yaml +22 -0
  263. package/presets/fleet/roles/Engineer.yaml +40 -0
  264. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  265. package/presets/fleet/room_layouts/pair.yaml +24 -0
  266. package/presets/fleet/room_layouts/single.yaml +17 -0
  267. package/presets/fleet/room_layouts/team.yaml +30 -0
  268. package/presets/fleet/room_templates/engineering.yaml +19 -0
  269. package/presets/manifest.json +2 -2
  270. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  271. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  272. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  273. package/dist/web-app/icons/ours-fleet.svg +0 -4
  274. package/dist/web-app/index.html +0 -17
  275. package/dist/web-app/manifest.webmanifest +0 -15
  276. package/dist/web-app/offline.html +0 -18
  277. package/dist/web-app/sw.js +0 -51
@@ -0,0 +1,48 @@
1
+ import type { FleetConfig } from '../config.js';
2
+ import { TaskStateError } from './task-state.js';
3
+ import { provisionMembers } from './provision.js';
4
+ import type { CoworkAdapter } from './cowork-adapter.js';
5
+ export declare class MemberAdditionRejected extends TaskStateError {
6
+ readonly accepted = false;
7
+ }
8
+ export interface AddMemberRequest {
9
+ requestId: string;
10
+ slot: string;
11
+ role: string;
12
+ brain: string;
13
+ agentTemplate: string;
14
+ }
15
+ export declare function memberAddition(taskId: string, requestId: string): {
16
+ requestId: string;
17
+ taskId: string;
18
+ roleId: string;
19
+ state: "running" | "succeeded" | "attention" | "failed";
20
+ error?: string;
21
+ } | {
22
+ state: string;
23
+ error: string;
24
+ requestId: string;
25
+ taskId: string;
26
+ roleId: string;
27
+ };
28
+ export declare function requestMemberAddition(input: {
29
+ taskId: string;
30
+ request: AddMemberRequest;
31
+ cfg: FleetConfig;
32
+ cowork: CoworkAdapter;
33
+ configPath?: string;
34
+ binPath: string;
35
+ provision?: typeof provisionMembers;
36
+ }): Promise<{
37
+ requestId: string;
38
+ taskId: string;
39
+ roleId: string;
40
+ state: "running" | "succeeded" | "attention" | "failed";
41
+ error?: string;
42
+ } | {
43
+ state: string;
44
+ error: string;
45
+ requestId: string;
46
+ taskId: string;
47
+ roleId: string;
48
+ }>;
@@ -0,0 +1,83 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { replaceFileAtomically, withFileLock } from '../atomic-file.js';
5
+ import { canonicalJson } from '../canonical-json.js';
6
+ import { stateRoot } from '../paths.js';
7
+ import { getTask, TaskStateError } from './task-state.js';
8
+ import { getRoomRecord } from './room-state.js';
9
+ import { prepareExecutionPlan } from './member-overrides.js';
10
+ import { sealTemplateSnapshot } from './templates.js';
11
+ import { provisionMembers } from './provision.js';
12
+ export class MemberAdditionRejected extends TaskStateError {
13
+ accepted = false;
14
+ }
15
+ const running = new Set();
16
+ function pathFor(taskId, requestId) {
17
+ if (!/^[a-z0-9]{10,40}$/.test(taskId) || !/^[a-zA-Z0-9_-]{8,80}$/.test(requestId))
18
+ throw new TaskStateError('Invalid task or request ID');
19
+ return join(stateRoot(), 'member-additions', taskId, requestId + '.json');
20
+ }
21
+ export function memberAddition(taskId, requestId) {
22
+ getTask(taskId);
23
+ const path = pathFor(taskId, requestId);
24
+ if (!existsSync(path))
25
+ throw new TaskStateError('Member request not found');
26
+ const receipt = JSON.parse(readFileSync(path, 'utf8'));
27
+ const { hash, ...view } = receipt;
28
+ return receipt.state === 'running' && !running.has(path) ? { ...view, state: 'attention', error: 'The server stopped observing this launch. Inspect the existing agent before any recovery.' } : view;
29
+ }
30
+ export async function requestMemberAddition(input) {
31
+ const { request: r, taskId } = input;
32
+ if (!r || typeof r.slot !== 'string' || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(r.slot)
33
+ || ![r.role, r.brain, r.agentTemplate].every(v => typeof v === 'string' && v.length > 0 && v.length <= 128))
34
+ // No receipt lookup has happened yet: this request ID may already be accepted.
35
+ throw new TaskStateError('Slot, Role, Brain and Agent Template are required');
36
+ const path = pathFor(taskId, r.requestId);
37
+ const hash = createHash('sha256').update(canonicalJson(r)).digest('hex');
38
+ mkdirSync(join(stateRoot(), 'member-additions', taskId), { recursive: true, mode: 0o700 });
39
+ return withFileLock(path + '.lock', async () => {
40
+ if (existsSync(path)) {
41
+ const old = JSON.parse(readFileSync(path, 'utf8'));
42
+ if (old.hash !== hash)
43
+ throw new TaskStateError('Request ID was already used with different settings');
44
+ return memberAddition(taskId, r.requestId);
45
+ }
46
+ const task = getTask(taskId);
47
+ const room = task.room_id && getRoomRecord(task.room_id);
48
+ if (!room || !['active', 'review'].includes(task.state) || task.terminal_intent || task.blocked || room.state !== 'active'
49
+ || room.task_id !== taskId || room.room_identity_cid !== task.room_identity_cid)
50
+ throw new MemberAdditionRejected('Adding an agent requires an active, unblocked task room');
51
+ const roleId = `${taskId.slice(0, 8)}-${r.slot}-1`;
52
+ if (room.member_seats.some(s => s.role_name === roleId))
53
+ throw new MemberAdditionRejected('This member slot already exists');
54
+ const plan = (() => {
55
+ try {
56
+ return prepareExecutionPlan({ name: 'additional-member', version: 1, description: 'Additional task participant',
57
+ contract: room.template_snapshot?.contract, members: [{ slot: r.slot, role: r.role, count: 1, agent_template: r.agentTemplate }] }, input.cfg, { [r.slot]: { role: r.role, brain: r.brain } });
58
+ }
59
+ catch (error) {
60
+ throw new MemberAdditionRejected(error instanceof Error ? error.message : 'Invalid member configuration');
61
+ }
62
+ })();
63
+ const template = sealTemplateSnapshot(plan.snapshot, input.cfg.agentTemplates ?? {}, plan.launchDefinitions);
64
+ const receipt = { taskId, requestId: r.requestId, roleId, hash, state: 'running' };
65
+ const save = () => replaceFileAtomically(path, JSON.stringify(receipt) + '\n', 0o600);
66
+ save();
67
+ running.add(path);
68
+ void (input.provision ?? provisionMembers)({ cfg: input.cfg, cowork: input.cowork, configPath: input.configPath, binPath: input.binPath,
69
+ taskId, roomId: room.room_id, template, goal: task.title, brief: task.brief, append: true }).then(result => {
70
+ const seat = result.member_seats.find(s => s.role_name === roleId);
71
+ receipt.state = seat?.seat_state === 'active' && getTask(taskId).member_roles.some(m => m.name === roleId) ? 'succeeded' : 'attention';
72
+ if (receipt.state === 'attention')
73
+ receipt.error = 'Admission is not confirmed. Inspect the existing launch; no automatic retry was made.';
74
+ }).catch(() => { receipt.state = 'failed'; receipt.error = 'Member launch failed. Inspect the task and agent logs before recovery.'; })
75
+ .finally(() => { try {
76
+ save();
77
+ }
78
+ finally {
79
+ running.delete(path);
80
+ } }).catch(() => { running.delete(path); });
81
+ return memberAddition(taskId, r.requestId);
82
+ });
83
+ }
@@ -0,0 +1,8 @@
1
+ import type { ArchivedMemberAbsence, RoomMemberSeat } from './types.js';
2
+ /** Revalidate live facts; a persisted timestamp is never cleanup authority. */
3
+ export declare function verifyArchivedAbsence(proof: ArchivedMemberAbsence): Promise<void>;
4
+ /** After the verified archive has been consumed by explicit workspace deletion,
5
+ * still fence replacement live state and a recreated daemon identity on every retry.
6
+ */
7
+ export declare function verifyArchivedMemberStillAbsent(proof: ArchivedMemberAbsence): Promise<void>;
8
+ export declare function proveArchivedAbsence(seat: RoomMemberSeat): Promise<ArchivedMemberAbsence>;
@@ -0,0 +1,48 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { agentDir } from '../paths.js';
4
+ import { tempArchiveForLaunch, tempArchiveForCreationAction, tempSupervisorLiveness } from '../temp-lifecycle.js';
5
+ import { assertMemberIdentityAbsent } from './close.js';
6
+ /** Revalidate live facts; a persisted timestamp is never cleanup authority. */
7
+ export async function verifyArchivedAbsence(proof) {
8
+ const absent = () => {
9
+ if (existsSync(agentDir(proof.name, true)))
10
+ throw new Error('Archived member has replacement live state');
11
+ };
12
+ absent();
13
+ const archive = tempArchiveForLaunch(proof.name, proof.launch_id);
14
+ const action = tempArchiveForCreationAction(proof.name, proof.action_id);
15
+ if (!archive || archive !== proof.archive_path || action?.path !== archive
16
+ || action.launchId !== proof.launch_id
17
+ || readFileSync(join(archive, '.identity'), 'utf8').trim() !== proof.name)
18
+ throw new Error('Archived member absence ownership proof mismatch');
19
+ if (await tempSupervisorLiveness(archive) !== 'stopped')
20
+ throw new Error('Archived member supervisor absence is not proven');
21
+ await verifyArchivedMemberStillAbsent(proof);
22
+ }
23
+ /** After the verified archive has been consumed by explicit workspace deletion,
24
+ * still fence replacement live state and a recreated daemon identity on every retry.
25
+ */
26
+ export async function verifyArchivedMemberStillAbsent(proof) {
27
+ const absent = () => {
28
+ if (existsSync(agentDir(proof.name, true)))
29
+ throw new Error('Archived member has replacement live state');
30
+ };
31
+ absent();
32
+ await assertMemberIdentityAbsent({
33
+ role_name: proof.name, slot: 'archived', cowork_role: 'archived', seat_state: 'removed',
34
+ });
35
+ absent();
36
+ }
37
+ export async function proveArchivedAbsence(seat) {
38
+ if (seat.identity_cid || seat.retirement?.phase !== 'identity_absent'
39
+ || !seat.launch?.launch_id || !seat.launch.action_id
40
+ || seat.launch.launch_id !== seat.retirement.launch_id || !seat.retirement.archive_path)
41
+ throw new Error('Missing exact archived member absence evidence');
42
+ const proof = {
43
+ name: seat.role_name, launch_id: seat.launch.launch_id, action_id: seat.launch.action_id,
44
+ archive_path: seat.retirement.archive_path, checked_at: new Date().toISOString(),
45
+ };
46
+ await verifyArchivedAbsence(proof);
47
+ return proof;
48
+ }
@@ -75,6 +75,11 @@ async function presentDetachedProvisioningOutcome(outcome) {
75
75
  function taskRoomPublicFailure(error) {
76
76
  const f = error.fields;
77
77
  switch (error.code) {
78
+ case 'task_not_ready': return {
79
+ legacy: `task readiness ${f.readiness}: ${f.reason}`, kind: 'state',
80
+ detail: `Task ${f.task} readiness is ${f.readiness}: ${f.reason}${f.member ? ` (${f.member})` : ''}.`,
81
+ action: 'Ask Fleet Coordinator to inspect room and member status and choose supported recovery. Do not blindly relaunch agents or reuse invites.',
82
+ };
78
83
  case 'task_confirmation_mismatch': return {
79
84
  legacy: 'confirmation ID must match task ID', kind: 'usage',
80
85
  detail: 'The two task IDs must match.', action: 'Repeat the same task ID twice.',
@@ -515,10 +520,10 @@ function taskRoomService(configuration) {
515
520
  function allTemplates(cfg) {
516
521
  return cfg.roomTemplates ?? {};
517
522
  }
518
- function coworkFor(cfg) {
523
+ function coworkFor(cfg, options) {
519
524
  if (!cfg.rooms)
520
525
  throw new ConfigError('rooms: configuration is required before creating or querying rooms');
521
- return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config });
526
+ return createCoworkAdapter({ configPath: cfg.rooms.cowork?.config, ...options });
522
527
  }
523
528
  function resolveRoomTemplate(cfg, name) {
524
529
  if (!name)
@@ -827,16 +832,18 @@ export function registerTaskCommands(parent, cOpt) {
827
832
  dieTaskRoom(e);
828
833
  }
829
834
  });
830
- taskCmd.command('show <id>')
835
+ cOpt(taskCmd.command('show <id>'))
831
836
  .description('show task details')
832
837
  .option('--json', 'JSON output')
833
838
  .action(async (id, opts) => {
834
839
  try {
835
- const service = taskRoomService();
840
+ const service = taskRoomService(opts.configuration);
836
841
  const { task: t, orchestration: room } = service.getTask(id);
842
+ const readiness = t.state === 'active' ? await service.observeTaskReadiness(id) : undefined;
837
843
  if (opts.json) {
838
844
  console.log(JSON.stringify({
839
845
  schema_version: 1, task: t, orchestration: room ?? null,
846
+ ...(readiness ? { readiness } : {}),
840
847
  }, null, 2));
841
848
  return;
842
849
  }
@@ -846,6 +853,8 @@ export function registerTaskCommands(parent, cOpt) {
846
853
  { label: 'ID', value: t.task_id, kind: 'code' },
847
854
  { label: 'Title', value: t.title },
848
855
  { label: 'Status', value: taskStatus(t.state), kind: 'markdown' },
856
+ ...(readiness ? [{ label: 'Readiness', value: readiness.state },
857
+ ...('reason' in readiness ? [{ label: 'Reason', value: readiness.reason }] : [])] : []),
849
858
  ...(t.blocked ? [{ label: 'Blocked', value: t.blocked.reason }] : []),
850
859
  ...(t.template ? [{ label: 'Template', value: `${t.template.name}@${t.template.version}`, kind: 'code' }] : []),
851
860
  ...(t.room_id ? [{ label: 'Room', value: t.room_id, kind: 'code' }] : []),
@@ -1152,7 +1161,7 @@ export function registerTaskCommands(parent, cOpt) {
1152
1161
  const app = taskRoomService(opts.configuration);
1153
1162
  for (;;) {
1154
1163
  const before = app.taskProvisioningOutcome(id);
1155
- if (before.kind !== 'in_progress') {
1164
+ if (before.kind !== 'in_progress' || before.next_action) {
1156
1165
  await presentDetachedProvisioningOutcome(before);
1157
1166
  return;
1158
1167
  }
@@ -1176,6 +1185,26 @@ export function registerTaskCommands(parent, cOpt) {
1176
1185
  dieTaskRoom(e);
1177
1186
  }
1178
1187
  });
1188
+ cOpt(taskCmd.command('_layout <id> <operation> [room]', { hidden: true }))
1189
+ .description('internal: run an accepted layout room operation (open, close-room, close)')
1190
+ .option('--json', 'JSON output')
1191
+ .option('--operation-id <id>', 'accepted operation ID; status updates apply only while it is current')
1192
+ .action(async (id, operation, room, opts) => {
1193
+ try {
1194
+ if (!['open', 'close-room', 'close'].includes(operation))
1195
+ throw new Error(`unknown layout operation: ${operation}`);
1196
+ const view = await taskRoomService(opts.configuration).runTaskLayoutOperation({
1197
+ actor: { kind: 'internal_worker', surface: 'cli' }, taskId: id,
1198
+ operation: operation, room, operationId: opts.operationId,
1199
+ });
1200
+ console.log(JSON.stringify({ schema_version: 1, layout: view }, null, 2));
1201
+ }
1202
+ catch (e) {
1203
+ if (opts.json)
1204
+ die(e);
1205
+ dieTaskRoom(e);
1206
+ }
1207
+ });
1179
1208
  cOpt(taskCmd.command('_settle_delete <id>', { hidden: true }))
1180
1209
  .description('internal: settle a previously accepted task deletion')
1181
1210
  .option('--json', 'JSON output')
@@ -2,6 +2,8 @@ import { withFileLock } from '../atomic-file.js';
2
2
  import { type TempLifecycleDeps } from '../temp-lifecycle.js';
3
3
  import { type CoworkAdapter } from './cowork-adapter.js';
4
4
  import type { RoomMemberSeat, RoomOrchestrationRecord } from './types.js';
5
+ export declare const CLOSE_LOCK_STALE_MS: number;
6
+ export declare function roomCloseLockPath(roomId: string): string;
5
7
  export interface RoomCloseDeps {
6
8
  inspectMember?(seat: RoomMemberSeat): Promise<{
7
9
  launchId: string;
@@ -19,12 +21,13 @@ export declare function waitForLivenessAbsent(role: string, launchId: string, li
19
21
  /** Report whether any daemon identity — under any name — carries this exact CID. */
20
22
  export declare function identityCidPresent(cid: string): Promise<boolean>;
21
23
  export declare function removeExactMemberIdentity(seat: RoomMemberSeat): Promise<void>;
24
+ export declare function assertMemberIdentityAbsent(seat: RoomMemberSeat): Promise<void>;
22
25
  /** One forward-only room close saga shared by every Fleet entry point. */
23
26
  export declare function acceptManagedRoomClose(roomId: string): Promise<RoomOrchestrationRecord>;
24
27
  export declare function recordManagedRoomCloseError(roomId: string, error: string, recoveryHint: string): Promise<RoomOrchestrationRecord>;
25
28
  export declare function closeManagedRoom(input: {
26
29
  roomId: string;
27
- cowork: Pick<CoworkAdapter, 'closeRoom'>;
30
+ cowork: Pick<CoworkAdapter, 'closeRoom'> & Partial<Pick<CoworkAdapter, 'getRoom'>>;
28
31
  deps?: RoomCloseDeps;
29
32
  }): Promise<RoomOrchestrationRecord>;
30
33
  export interface ManagedRoomDeleteResult {
@@ -38,6 +41,6 @@ export declare function deleteLegacyClosedRooms(input: {
38
41
  /** Retire live resources through the existing cursor, then delete retained state. */
39
42
  export declare function deleteManagedRoom(input: {
40
43
  roomId: string;
41
- cowork: Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'>;
44
+ cowork: Pick<CoworkAdapter, 'closeRoom' | 'deleteRoom'> & Partial<Pick<CoworkAdapter, 'getRoom'>>;
42
45
  deps?: RoomCloseDeps;
43
46
  }): Promise<ManagedRoomDeleteResult>;
@@ -1,16 +1,22 @@
1
+ import { detachDeletedRoom } from './task-state.js';
2
+ import { binderKey } from '../agent-ours/state.js';
3
+ import { eraseMemberArtifacts } from './erasure.js';
4
+ import { deleteWorkspace, assertWorkspaceDeletable } from './workspace.js';
5
+ import { collectWorkspaceArchives } from './workspace-artifacts.js';
1
6
  import { existsSync, readFileSync } from 'node:fs';
2
7
  import { join } from 'node:path';
3
8
  import { randomUUID } from 'node:crypto';
4
9
  import { attachOursClient } from '@ours.network/sdk/client';
5
10
  import { withFileLock } from '../atomic-file.js';
6
11
  import { agentDir, stateRoot } from '../paths.js';
7
- import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, } from '../temp-lifecycle.js';
12
+ import { readClientProfile } from '../client-profile.js';
13
+ import { readTempSupervisor, secureStoppedTempArchive, stopTempSupervisor, tempSupervisorLiveness, tempArchiveForLaunch, tempArchiveForCreationAction, } from '../temp-lifecycle.js';
8
14
  import { CoworkProtocolError } from './cowork-adapter.js';
9
- import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, } from './room-state.js';
10
- const CLOSE_LOCK_STALE_MS = 5 * 60_000;
15
+ import { advanceMemberRetirement, advanceRoomClose, beginRoomClose, closeRoom, deleteRoomRecord, getRoomRecord, listRoomRecords, setRoomCloseError, recordRetirementMemberCids, } from './room-state.js';
16
+ export const CLOSE_LOCK_STALE_MS = 5 * 60_000;
11
17
  const STOP_POLLS = 50;
12
18
  const STOP_POLL_MS = 100;
13
- function roomCloseLockPath(roomId) {
19
+ export function roomCloseLockPath(roomId) {
14
20
  return join(stateRoot(), 'locks', 'room-close', encodeURIComponent(roomId));
15
21
  }
16
22
  function errorText(error) {
@@ -19,6 +25,20 @@ function errorText(error) {
19
25
  function exactMemberIdentity(seat) {
20
26
  const dir = agentDir(seat.role_name, true);
21
27
  const identityPath = join(dir, '.identity');
28
+ if (existsSync(dir) && !existsSync(identityPath) && seat.launch?.action_id && seat.launch.launch_id) {
29
+ const supervisor = readTempSupervisor(dir);
30
+ let creation;
31
+ try {
32
+ creation = JSON.parse(readFileSync(join(dir, 'creation.json'), 'utf8'));
33
+ }
34
+ catch (error) {
35
+ if (error.code !== 'ENOENT')
36
+ throw error;
37
+ }
38
+ if (supervisor?.role === seat.role_name && supervisor.launchId === seat.launch.launch_id
39
+ && creation?.role === seat.role_name && creation.creationActionId === seat.launch.action_id)
40
+ return;
41
+ }
22
42
  if (!existsSync(dir) || !existsSync(identityPath)) {
23
43
  throw new Error(`room member '${seat.role_name}' has no live Fleet temp-state identity proof; refusing retirement`);
24
44
  }
@@ -30,7 +50,8 @@ function exactMemberIdentity(seat) {
30
50
  export async function inspectMember(seat) {
31
51
  exactMemberIdentity(seat);
32
52
  const supervisor = readTempSupervisor(agentDir(seat.role_name, true));
33
- if (!supervisor || supervisor.role !== seat.role_name) {
53
+ if (!supervisor || supervisor.role !== seat.role_name
54
+ || (seat.launch?.launch_id && supervisor.launchId !== seat.launch.launch_id)) {
34
55
  throw new Error(`room member '${seat.role_name}' has no exact Fleet supervisor ownership proof`);
35
56
  }
36
57
  return { launchId: supervisor.launchId };
@@ -54,16 +75,24 @@ export async function waitForLivenessAbsent(role, launchId, lifecycleDeps = {})
54
75
  throw new Error(`temporary role '${role}' did not reach proven stopped liveness`);
55
76
  }
56
77
  async function withIdentityClient(work) {
78
+ const profile = readClientProfile(process.env);
79
+ const leaseToken = `ours-fleet-room-close-${process.pid}-${randomUUID()}`;
57
80
  const client = await attachOursClient({
58
- env: process.env,
59
- leaseToken: `ours-fleet-room-close-${process.pid}-${randomUUID()}`,
60
- clientPid: process.pid,
81
+ endpoint: profile.endpoint,
82
+ expectedInstanceId: profile.expectedInstanceId,
83
+ credentialPath: profile.credentialPath,
84
+ sessionMode: 'external', env: {}, leaseToken,
61
85
  });
62
86
  try {
63
87
  return await work(client);
64
88
  }
65
89
  finally {
66
- await client.releaseLease().catch(() => { });
90
+ try {
91
+ await client.releaseLease().catch(() => { });
92
+ }
93
+ finally {
94
+ await client.close();
95
+ }
67
96
  }
68
97
  }
69
98
  function listedIdentity(rows, name) {
@@ -78,14 +107,19 @@ export async function identityCidPresent(cid) {
78
107
  }
79
108
  export async function removeExactMemberIdentity(seat) {
80
109
  await withIdentityClient(async (client) => {
81
- const before = listedIdentity(await client.listIdentities(), seat.role_name);
82
- if (!before)
110
+ const rows = await client.listIdentities();
111
+ const before = listedIdentity(rows, seat.role_name);
112
+ if (!before) {
113
+ if (rows.some(row => row.name === seat.role_name || (seat.identity_cid && 'cid' in row
114
+ && row.cid.toLowerCase() === seat.identity_cid.toLowerCase())))
115
+ throw new Error(`room member '${seat.role_name}' identity absence is not proven`);
83
116
  return;
117
+ }
84
118
  if (!seat.identity_cid) {
85
- throw new Error(`room member '${seat.role_name}' exists without a recorded authenticated CID; refusing removal`);
119
+ throw new Error(`room member '${seat.role_name}' exists without a recorded authenticated CID; identity absence is not proven; refusing removal`);
86
120
  }
87
121
  if (before.cid?.toLowerCase() !== seat.identity_cid.toLowerCase()) {
88
- throw new Error(`room member '${seat.role_name}' identity CID mismatch: recorded ${seat.identity_cid}, found ${before.cid ?? 'none'}`);
122
+ throw new Error(`room member '${seat.role_name}' identity absence is not proven: CID mismatch: recorded ${seat.identity_cid}, found ${before.cid ?? 'none'}`);
89
123
  }
90
124
  try {
91
125
  await client.removeIdentity({ name: seat.role_name });
@@ -100,17 +134,59 @@ export async function removeExactMemberIdentity(seat) {
100
134
  if (after)
101
135
  throw error;
102
136
  }
103
- const after = listedIdentity(await client.listIdentities(), seat.role_name);
104
- if (after) {
137
+ const after = await client.listIdentities();
138
+ if (after.some(row => row.name === seat.role_name || (seat.identity_cid && 'cid' in row
139
+ && row.cid.toLowerCase() === seat.identity_cid.toLowerCase()))) {
105
140
  throw new Error(`room member '${seat.role_name}' identity still exists after remove_identity`);
106
141
  }
107
142
  });
108
143
  }
144
+ export async function assertMemberIdentityAbsent(seat) {
145
+ await withIdentityClient(async (client) => {
146
+ const rows = await client.listIdentities();
147
+ if (rows.some(row => row.name === seat.role_name ||
148
+ (seat.identity_cid && 'cid' in row && row.cid.toLowerCase() === seat.identity_cid.toLowerCase()))) {
149
+ throw new Error(`room member '${seat.role_name}' identity absence is not proven; refusing retirement`);
150
+ }
151
+ });
152
+ }
109
153
  async function retireMember(roomId, seat, deps) {
110
154
  let room = getRoomRecord(roomId);
111
155
  let current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
112
156
  let retirement = current.retirement;
157
+ if (retirement?.phase === 'identity_absent') {
158
+ if (!deps.inspectMember) {
159
+ if (existsSync(agentDir(current.role_name, true)))
160
+ throw new Error('Retired member has replacement launch live state');
161
+ await assertMemberIdentityAbsent(current);
162
+ }
163
+ return;
164
+ }
113
165
  if (!retirement) {
166
+ if (!existsSync(agentDir(current.role_name, true)) && current.launch?.launch_id && current.launch.action_id) {
167
+ const archived = tempArchiveForLaunch(current.role_name, current.launch.launch_id);
168
+ const created = tempArchiveForCreationAction(current.role_name, current.launch.action_id);
169
+ if (archived && (!created || created.path !== archived || created.launchId !== current.launch.launch_id))
170
+ throw new Error('Archived member creation ownership mismatch');
171
+ if (archived && created?.path === archived && created.launchId === current.launch.launch_id) {
172
+ if (readFileSync(join(archived, '.identity'), 'utf8').trim() !== current.role_name)
173
+ throw new Error(`room member '${current.role_name}' archive identity mismatch`);
174
+ if (await tempSupervisorLiveness(archived) !== 'stopped')
175
+ throw new Error(`room member '${current.role_name}' archived supervisor is not proven stopped`);
176
+ // A terminated launch can archive itself before room retirement begins.
177
+ // Accept its exact durable provenance only when no identity needs removal.
178
+ await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
179
+ await assertMemberIdentityAbsent(current);
180
+ const latest = getRoomRecord(roomId)?.member_seats.find(seat => seat.role_name === current.role_name);
181
+ if (existsSync(agentDir(current.role_name, true))
182
+ || latest?.launch?.launch_id !== current.launch.launch_id
183
+ || latest?.launch?.action_id !== current.launch.action_id
184
+ || latest?.identity_cid !== current.identity_cid)
185
+ throw new Error(`room member '${current.role_name}' changed during archived retirement proof`);
186
+ advanceMemberRetirement(roomId, current.role_name, 'identity_absent', current.launch.launch_id, archived);
187
+ return;
188
+ }
189
+ }
114
190
  if (current.launch?.state === 'pending' && current.launch.attempt === 0) {
115
191
  if (existsSync(agentDir(current.role_name, true))) {
116
192
  throw new Error(`never-launched room member '${current.role_name}' unexpectedly has Fleet temp state`);
@@ -119,12 +195,31 @@ async function retireMember(roomId, seat, deps) {
119
195
  advanceMemberRetirement(roomId, current.role_name, 'identity_absent', 'never-launched');
120
196
  return;
121
197
  }
198
+ if (!deps.inspectMember && !existsSync(agentDir(current.role_name, true))) {
199
+ await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
200
+ if (existsSync(agentDir(current.role_name, true)))
201
+ throw new Error(`room member '${current.role_name}' acquired replacement state during retirement`);
202
+ advanceMemberRetirement(roomId, current.role_name, 'identity_absent', 'absent-verified');
203
+ return;
204
+ }
122
205
  const ownership = await (deps.inspectMember ?? inspectMember)(current);
123
206
  room = advanceMemberRetirement(roomId, current.role_name, 'stop_requested', ownership.launchId);
124
207
  current = room.member_seats.find(candidate => candidate.role_name === seat.role_name);
125
208
  retirement = current.retirement;
126
209
  }
210
+ if (['stop_requested', 'liveness_absent', 'archive_secured'].includes(retirement.phase) && !deps.requestStop
211
+ && !existsSync(agentDir(current.role_name, true))) {
212
+ const archive = tempArchiveForLaunch(current.role_name, retirement.launch_id);
213
+ if (archive && await tempSupervisorLiveness(archive) !== 'stopped')
214
+ throw new Error('Archived supervisor is not proven stopped');
215
+ await (deps.removeIdentity ?? removeExactMemberIdentity)(current);
216
+ await assertMemberIdentityAbsent(current);
217
+ advanceMemberRetirement(roomId, current.role_name, 'identity_absent', retirement.launch_id, archive, !archive);
218
+ return;
219
+ }
127
220
  if (retirement.phase === 'stop_requested') {
221
+ if (!deps.inspectMember && (await inspectMember(current)).launchId !== retirement.launch_id)
222
+ throw new Error('Member launch changed before stop');
128
223
  await (deps.requestStop ?? (async (role) => { await stopTempSupervisor(role); }))(current.role_name);
129
224
  await (deps.waitForLivenessAbsent ?? waitForLivenessAbsent)(current.role_name, retirement.launch_id);
130
225
  room = advanceMemberRetirement(roomId, current.role_name, 'liveness_absent', retirement.launch_id);
@@ -154,19 +249,31 @@ export async function closeManagedRoom(input) {
154
249
  const lock = deps.lock ?? withFileLock;
155
250
  return lock(roomCloseLockPath(input.roomId), async () => {
156
251
  let room = beginRoomClose(input.roomId);
157
- if (room.state === 'closed')
252
+ if (room.state === 'closed') {
253
+ if (!deps.inspectMember)
254
+ for (const seat of room.member_seats) {
255
+ if (existsSync(agentDir(seat.role_name, true)))
256
+ throw new Error('Closed room has replacement live state');
257
+ await assertMemberIdentityAbsent(seat);
258
+ }
158
259
  return room;
260
+ }
159
261
  try {
160
262
  if (room.close?.phase === 'retire_members') {
263
+ room = await recoverMemberCids(room, input.cowork);
161
264
  for (const seat of room.member_seats) {
162
- if (seat.retirement?.phase === 'identity_absent')
163
- continue;
164
265
  await retireMember(input.roomId, seat, deps);
165
266
  }
166
267
  room = advanceRoomClose(input.roomId, 'close_cowork');
167
268
  }
168
269
  if (room.close?.phase === 'close_cowork') {
169
- await input.cowork.closeRoom(input.roomId);
270
+ try {
271
+ await input.cowork.closeRoom(input.roomId);
272
+ }
273
+ catch (error) {
274
+ if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
275
+ throw error;
276
+ }
170
277
  }
171
278
  return closeRoom(input.roomId);
172
279
  }
@@ -180,6 +287,9 @@ export async function closeManagedRoom(input) {
180
287
  export async function deleteLegacyClosedRooms(input) {
181
288
  const deleted = [];
182
289
  for (const room of listRoomRecords({ state: 'closed' })) {
290
+ // This compatibility sweep is retirement, never explicit workspace deletion.
291
+ if (room.workspace)
292
+ continue;
183
293
  try {
184
294
  await input.cowork.deleteRoom(room.room_id);
185
295
  }
@@ -196,14 +306,84 @@ export async function deleteLegacyClosedRooms(input) {
196
306
  }
197
307
  /** Retire live resources through the existing cursor, then delete retained state. */
198
308
  export async function deleteManagedRoom(input) {
199
- await closeManagedRoom(input);
200
- try {
201
- await input.cowork.deleteRoom(input.roomId);
202
- }
203
- catch (error) {
204
- setRoomCloseError(input.roomId, errorText(error), `Retry 'ours-fleet room delete ${input.roomId} ${input.roomId}'.`);
205
- throw error;
309
+ if (getRoomRecord(input.roomId))
310
+ await closeManagedRoom(input);
311
+ return withFileLock(roomCloseLockPath(input.roomId), async () => {
312
+ try {
313
+ try {
314
+ await input.cowork.deleteRoom(input.roomId);
315
+ }
316
+ catch (error) {
317
+ // An earlier explicit delete may have removed the remote room before
318
+ // local filesystem cleanup failed. Never restore it or repeat creation.
319
+ if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
320
+ throw error;
321
+ }
322
+ const retained = getRoomRecord(input.roomId);
323
+ if (retained?.workspace && !retained.task_id) {
324
+ assertWorkspaceDeletable(retained.workspace, 'room', input.roomId);
325
+ collectWorkspaceArchives(retained.workspace);
326
+ deleteWorkspace(retained.workspace, 'room', input.roomId);
327
+ }
328
+ if (retained)
329
+ await eraseMemberArtifacts('room', input.roomId, retained.member_seats, [input.roomId]);
330
+ if (retained?.task_id)
331
+ detachDeletedRoom(retained.task_id, input.roomId);
332
+ deleteRoomRecord(input.roomId);
333
+ return { room_id: input.roomId, deleted: true };
334
+ }
335
+ catch (error) {
336
+ if (getRoomRecord(input.roomId))
337
+ setRoomCloseError(input.roomId, errorText(error), `Fix the cleanup error and explicitly retry room delete ${input.roomId} ${input.roomId}.`);
338
+ throw error;
339
+ }
340
+ }, {}, CLOSE_LOCK_STALE_MS);
341
+ }
342
+ /** Recover only an authenticated seat tied to this exact room and issued invite.
343
+ * Display names and the current daemon name inventory are never binding proof.
344
+ */
345
+ async function recoverMemberCids(room, cowork) {
346
+ if (!room.member_seats.some(seat => !seat.identity_cid))
347
+ return room;
348
+ const profile = room.member_seats.some(seat => seat.launch?.action_id) ? readClientProfile(process.env) : undefined;
349
+ let remote;
350
+ if (cowork.getRoom && room.room_identity_cid) {
351
+ try {
352
+ remote = await cowork.getRoom(room.room_id);
353
+ }
354
+ catch (error) {
355
+ if (!(error instanceof CoworkProtocolError && error.code === 'not_found'))
356
+ throw error;
357
+ }
358
+ if (remote && (remote.room_id !== room.room_id || remote.identity_cid.toLowerCase() !== room.room_identity_cid.toLowerCase()))
359
+ throw new Error('Room identity mismatch during deletion recovery');
206
360
  }
207
- deleteRoomRecord(input.roomId);
208
- return { room_id: input.roomId, deleted: true };
361
+ const seats = room.member_seats.map(seat => {
362
+ let runtimeCid;
363
+ if (profile && seat.launch?.action_id) {
364
+ const dir = join(stateRoot(), 'private-ours', binderKey(profile.expectedInstanceId, seat.role_name));
365
+ try {
366
+ const state = JSON.parse(readFileSync(join(dir, 'state.json'), 'utf8'));
367
+ const instance = JSON.parse(readFileSync(join(dir, 'instance.json'), 'utf8'));
368
+ if (state.name === seat.role_name && state.daemon === profile.expectedInstanceId
369
+ && state.lifetime === 'temporary' && state.action === seat.launch.action_id
370
+ && instance.role === seat.role_name && instance.temporary === true && instance.instance === state.instance)
371
+ runtimeCid = state.cid;
372
+ }
373
+ catch (error) {
374
+ if (error.code !== 'ENOENT')
375
+ throw error;
376
+ }
377
+ }
378
+ const matches = seat.invite_id ? remote?.seats.filter(value => value.invite_id === seat.invite_id && value.role === seat.cowork_role) ?? [] : [];
379
+ if (matches.length > 1)
380
+ throw new Error('Ambiguous authenticated member identity during deletion recovery');
381
+ const candidates = [seat.identity_cid, runtimeCid, matches[0]?.identity_cid].filter((v) => !!v);
382
+ if (candidates.some(cid => !/^[a-f0-9]{64}$/i.test(cid)))
383
+ throw new Error('Invalid authenticated member CID');
384
+ if (new Set(candidates.map(cid => cid.toLowerCase())).size > 1)
385
+ throw new Error('Contradictory authenticated member CID evidence');
386
+ return candidates.length ? { ...seat, identity_cid: candidates[0] } : seat;
387
+ });
388
+ return recordRetirementMemberCids(room.room_id, seats);
209
389
  }