@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
@@ -1,3 +1,4 @@
1
+ import { type TaskNoticeBinding, type TaskCreatedNotice } from './task-notice.js';
1
2
  import * as acp from '@agentclientprotocol/sdk';
2
3
  import type { CommonPermissions } from '../config.js';
3
4
  import type { AcpMcpServer } from '../harness/types.js';
@@ -340,6 +341,8 @@ export declare class AcpSession implements AgentSession {
340
341
  private codexMessagePhase;
341
342
  /** Normalize every ACP update losslessly into the durable ledger. */
342
343
  private recordConversationUpdate;
344
+ taskNoticeBinding(): TaskNoticeBinding | undefined;
345
+ recordTaskCreated(binding: TaskNoticeBinding, notice: TaskCreatedNotice): void;
343
346
  conversationPage(request?: {
344
347
  after?: string;
345
348
  limit?: number;
@@ -1,3 +1,4 @@
1
+ import { appendTaskCreated } from './task-notice.js';
1
2
  import { spawn } from 'node:child_process';
2
3
  import { createHash, randomUUID } from 'node:crypto';
3
4
  import { existsSync, lstatSync, readFileSync, readlinkSync, realpathSync, writeFileSync, } from 'node:fs';
@@ -760,6 +761,8 @@ export class AcpSession {
760
761
  const existing = this.conversation.receiptFor(command.commandId, bodyDigest);
761
762
  if (existing)
762
763
  return existing;
764
+ if (command.expectedSessionGeneration !== undefined && command.expectedSessionGeneration !== this.conversationSnapshot().sessionGeneration)
765
+ throw new Error('session_changed: Agent session changed. Record a new voice message.');
763
766
  const queued = await this.queuePrompt(command.text, {
764
767
  origin: { kind: 'owner-admin-console', commandId: command.commandId },
765
768
  actor: { browserSession: command.actorBrowserSession },
@@ -1805,6 +1808,10 @@ export class AcpSession {
1805
1808
  });
1806
1809
  }
1807
1810
  // ── conversation ledger access (AgentSession) ─────────────────────────────
1811
+ taskNoticeBinding() {
1812
+ return this.activeTurn && this.sessionId ? { sessionGeneration: this.sessionGeneration, acpSessionId: this.sessionId, promptId: this.activeTurn.id } : undefined;
1813
+ }
1814
+ recordTaskCreated(binding, notice) { appendTaskCreated(this.conversation, binding, notice); }
1808
1815
  conversationPage(request = {}) {
1809
1816
  const floor = Number(this.conversationStartCursor ?? 0);
1810
1817
  const requested = Number(request.after ?? 0);
@@ -23,6 +23,7 @@ export declare class RoleTurnArbiter implements AgentSession {
23
23
  private tail;
24
24
  private unsettled;
25
25
  private stopping;
26
+ private retiring;
26
27
  /** Bumped by `retireStalledAdmission`; every claim carries the one it was made under. */
27
28
  private generation;
28
29
  /** Resolves the instant the CURRENT generation is retired. */
@@ -62,6 +63,8 @@ export declare class RoleTurnArbiter implements AgentSession {
62
63
  tryScheduled(text: string, origin: Extract<PromptOrigin, {
63
64
  kind: 'scheduled-loop';
64
65
  }>, beforeQueue?: () => void | Promise<void>): Promise<ScheduledAttempt>;
66
+ /** Retire only at the same admission boundary used by all prompt producers. */
67
+ retireIfIdle(expired: (snapshot: SessionSnapshot, queueDepth: number) => boolean): Promise<boolean>;
65
68
  stopScheduledAdmission(): void;
66
69
  isAlive(): boolean;
67
70
  snapshot(): SessionSnapshot;
@@ -29,6 +29,7 @@ export class RoleTurnArbiter {
29
29
  tail = Promise.resolve();
30
30
  unsettled = 0;
31
31
  stopping = false;
32
+ retiring = false;
32
33
  /** Bumped by `retireStalledAdmission`; every claim carries the one it was made under. */
33
34
  generation = 0;
34
35
  /** Resolves the instant the CURRENT generation is retired. */
@@ -89,7 +90,8 @@ export class RoleTurnArbiter {
89
90
  retired.retire();
90
91
  }
91
92
  queuePrompt(text, options = {}) {
92
- return this.exclusive(async () => this.track(await this.session.queuePrompt(text, options)));
93
+ return this.exclusive(async () => { if (this.retiring)
94
+ throw new Error('Chat is closing'); return this.track(await this.session.queuePrompt(text, options)); });
93
95
  }
94
96
  async submitPrompt(text, options = {}) {
95
97
  return (await this.queuePrompt(text, options)).completion;
@@ -99,6 +101,8 @@ export class RoleTurnArbiter {
99
101
  * answers and explicit interrupts must remain able to pass immediately.
100
102
  */
101
103
  submitPromptAfterTool(text, options = {}) {
104
+ if (this.retiring)
105
+ return Promise.reject(new Error('Chat is closing'));
102
106
  return this.session.submitPromptAfterTool?.(text, options)
103
107
  ?? this.session.submitPrompt(text, { ...options, interrupt: false, steer: false });
104
108
  }
@@ -136,6 +140,19 @@ export class RoleTurnArbiter {
136
140
  throw error;
137
141
  });
138
142
  }
143
+ /** Retire only at the same admission boundary used by all prompt producers. */
144
+ retireIfIdle(expired) {
145
+ return this.exclusive(async () => {
146
+ const snapshot = this.session.snapshot();
147
+ const queue = this.session.conversationSnapshot?.().queueDepth ?? -1;
148
+ if (this.retiring || this.unsettled > 0 || snapshot.readiness !== 'idle' || !expired(snapshot, queue))
149
+ return false;
150
+ this.retiring = true;
151
+ this.stopping = true;
152
+ await this.session.close();
153
+ return true;
154
+ });
155
+ }
139
156
  stopScheduledAdmission() { this.stopping = true; }
140
157
  isAlive() { return this.session.isAlive(); }
141
158
  snapshot() { return this.session.snapshot(); }
@@ -157,7 +174,8 @@ export class RoleTurnArbiter {
157
174
  submitPromptBrowser(command) {
158
175
  if (!this.session.submitPromptBrowser)
159
176
  return Promise.reject(new Error('browser prompt admission is unavailable'));
160
- return this.exclusive(() => this.session.submitPromptBrowser(command));
177
+ return this.exclusive(() => { if (this.retiring)
178
+ throw new Error('Chat is closing'); return this.session.submitPromptBrowser(command); });
161
179
  }
162
180
  interrupt(source = 'local-console') {
163
181
  return this.exclusive(async () => interruptOutcome(await this.session.interrupt(source)));
@@ -55,7 +55,7 @@ export class CodexAppServerTransport {
55
55
  throw new Error('Codex app-server command is empty');
56
56
  const child = spawn(options.argv[0], options.argv.slice(1), {
57
57
  cwd: options.cwd,
58
- env: { ...process.env, ...options.env },
58
+ env: { ...(options.env?.FLEET_OURS_MANAGED === '1' ? {} : process.env), ...options.env },
59
59
  stdio: ['pipe', 'pipe', 'pipe'],
60
60
  });
61
61
  // `pid` is assigned synchronously on a successful local spawn. Checking it
@@ -1,3 +1,4 @@
1
+ import { type TaskNoticeBinding, type TaskCreatedNotice } from './task-notice.js';
1
2
  import type { CommonPermissions } from '../config.js';
2
3
  import type { ConversationEventV1, ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
3
4
  import type { CodexAppServerTransportFactory } from './codex-app-server-transport.js';
@@ -76,6 +77,8 @@ export declare class CodexAppServerSession implements AgentSession {
76
77
  setControllerAttached(attached: boolean): void;
77
78
  exitResult(): ExitRecord | null;
78
79
  close(): Promise<void>;
80
+ taskNoticeBinding(): TaskNoticeBinding | undefined;
81
+ recordTaskCreated(binding: TaskNoticeBinding, notice: TaskCreatedNotice): void;
79
82
  conversationPage(request?: {
80
83
  after?: string;
81
84
  limit?: number;
@@ -1,6 +1,8 @@
1
+ import { appendTaskCreated } from './task-notice.js';
1
2
  import { createHash, randomUUID } from 'node:crypto';
2
3
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
4
  import { isAbsolute, join, resolve } from 'node:path';
5
+ import { boundedJson } from './conversation-normalizer.js';
4
6
  import { ConversationEventStore } from './conversation-store.js';
5
7
  import { SessionEvents } from './events.js';
6
8
  import { CodexAppServerTransport } from './codex-app-server-transport.js';
@@ -228,8 +230,10 @@ export class CodexAppServerSession {
228
230
  }
229
231
  async interrupt(source = 'local-console') {
230
232
  const active = this.activeTurn;
231
- if (!active || !this.threadId || !active.nativeTurnId)
233
+ if (!active || !this.threadId)
232
234
  return { state: 'settled' };
235
+ if (!active.nativeTurnId)
236
+ throw new SessionControlError('backend', 'The operation is still pending without a native turn ID. Cancellation is not confirmed; inspect or restart the agent if it does not progress.');
233
237
  active.cancellationSource ??= source;
234
238
  this.events.emit('state', { turnId: active.promptId, status: 'running', origin: active.origin });
235
239
  this.conversation.appendSafe({
@@ -335,6 +339,10 @@ export class CodexAppServerSession {
335
339
  this.conversation.close();
336
340
  await this.transport.close();
337
341
  }
342
+ taskNoticeBinding() {
343
+ return this.activeTurn && this.threadId ? { sessionGeneration: this.sessionGeneration, acpSessionId: this.threadId, promptId: this.activeTurn.promptId } : undefined;
344
+ }
345
+ recordTaskCreated(binding, notice) { appendTaskCreated(this.conversation, binding, notice); }
338
346
  conversationPage(request = {}) {
339
347
  const floor = Number(this.conversationStartCursor ?? 0);
340
348
  const requested = Number(request.after ?? 0);
@@ -367,6 +375,8 @@ export class CodexAppServerSession {
367
375
  const existing = this.conversation.receiptFor(command.commandId, bodyDigest);
368
376
  if (existing)
369
377
  return existing;
378
+ if (command.expectedSessionGeneration !== undefined && command.expectedSessionGeneration !== this.conversationSnapshot().sessionGeneration)
379
+ throw new Error('session_changed: Agent session changed. Record a new voice message.');
370
380
  const queued = await this.queuePrompt(command.text, {
371
381
  origin: { kind: 'owner-admin-console', commandId: command.commandId },
372
382
  actor: { browserSession: command.actorBrowserSession },
@@ -415,6 +425,10 @@ export class CodexAppServerSession {
415
425
  this.reasoningEffort = { value: effort };
416
426
  this.readiness = 'idle';
417
427
  this.events.emit('state', { status: 'idle', text: `Codex thread ${this.threadId}` });
428
+ this.conversation.appendSafe({
429
+ kind: 'capabilities.updated', sessionGeneration: this.sessionGeneration,
430
+ payload: { commands: [{ name: 'compact', description: textBlock('Compact session context') }] },
431
+ });
418
432
  this.conversation.appendSafe({
419
433
  kind: 'session.state', sessionGeneration: this.sessionGeneration,
420
434
  payload: { status: 'idle' },
@@ -486,6 +500,12 @@ export class CodexAppServerSession {
486
500
  promptId, turnId: promptId, source: sourceFor(origin).source, payload: {},
487
501
  });
488
502
  try {
503
+ if (text.trim() === '/compact') {
504
+ // The acknowledgement is not completion. Standard turn notifications
505
+ // settle the same queued prompt and durable browser receipt.
506
+ await this.transport.request('thread/compact/start', { threadId: this.threadId });
507
+ return await settled;
508
+ }
489
509
  const response = await this.transport.request('turn/start', {
490
510
  threadId: this.threadId,
491
511
  input: [{ type: 'text', text, text_elements: [] }],
@@ -746,6 +766,10 @@ export class CodexAppServerSession {
746
766
  toolCallId: scheduled ? 'scheduled-loop-tool' : id, snapshot,
747
767
  title: scheduled ? 'scheduled-loop tool' : tool.title, status: tool.status,
748
768
  kind: string(item.type),
769
+ ...(!scheduled && item.arguments !== undefined ? { rawInput: boundedJson(item.arguments) } : {}),
770
+ ...(!scheduled && item.type === 'commandExecution' ? { rawInput: boundedJson({ command: item.command, cwd: item.cwd }), ...(item.aggregatedOutput !== undefined ? { rawOutput: boundedJson({ output: item.aggregatedOutput, exitCode: item.exitCode }) } : {}) } : {}),
771
+ ...(!scheduled && item.type === 'fileChange' && item.changes !== undefined ? { rawOutput: boundedJson(item.changes) } : {}),
772
+ ...(!scheduled && (item.result !== undefined || item.error !== undefined) ? { rawOutput: boundedJson({ result: item.result, error: item.error }) } : {}),
749
773
  },
750
774
  });
751
775
  }
@@ -1,3 +1,5 @@
1
+ import type { LayoutControlRequest } from '../rooms-tasks/layout-control.js';
2
+ import type { SupervisorCorrespondence, AgentHistoryQuery } from '../agent-ours/correspondence.js';
1
3
  import { type Socket } from 'node:net';
2
4
  import type { AgentSession, ControlFailureKind, SessionEvent, SessionSnapshot } from './types.js';
3
5
  import type { OwnerChannelHandle, OwnerChannelManagementRequest } from '../owner-channel/channel.js';
@@ -9,8 +11,11 @@ export interface ControlRequest {
9
11
  version: 1 | 2 | 3;
10
12
  id: string;
11
13
  token: string;
12
- command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn' | 'fleet_audit_begin' | 'fleet_audit_present' | 'fleet_audit_finish';
14
+ command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'submit_voice_prompt' | 'interrupt_v2' | 'layout_control' | 'agent_contacts' | 'agent_history' | 'respond_permission_v2' | 'fleet_spawn' | 'fleet_audit_begin' | 'fleet_audit_present' | 'fleet_audit_finish';
15
+ layout?: LayoutControlRequest;
16
+ agentHistory?: AgentHistoryQuery;
13
17
  text?: string;
18
+ expectedSessionGeneration?: string;
14
19
  permissionId?: string;
15
20
  optionId?: string;
16
21
  since?: number;
@@ -112,9 +117,13 @@ export declare class RoleControlServer {
112
117
  private readonly sockets;
113
118
  /** Interrupt idempotency: same command id returns the first receipt. */
114
119
  private readonly interruptCommands;
120
+ private correspondence?;
121
+ setCorrespondence(value: SupervisorCorrespondence): void;
115
122
  private ownerChannel?;
116
123
  private loopManager?;
117
124
  private reloadConfig?;
125
+ private layoutControl?;
126
+ setLayoutControl(handler: (request: LayoutControlRequest) => Promise<unknown>): void;
118
127
  private fleetSpawner?;
119
128
  private fleetAuditor?;
120
129
  constructor(stateDir: string, session: AgentSession, log: (line: string) => void);
@@ -1,3 +1,4 @@
1
+ import { socketPath as privateSocketPath } from '../socket-path.js';
1
2
  import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
3
  import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
4
  import { createConnection, createServer } from 'node:net';
@@ -8,7 +9,7 @@ import { interruptSession, queueSessionPrompt, respondSessionPermission, respond
8
9
  const MAX_LINE_BYTES = 64 * 1024;
9
10
  /** Commands that require protocol version 3. */
10
11
  const V3_COMMANDS = new Set([
11
- 'conversation_page', 'conversation_follow', 'submit_prompt_v2', 'interrupt_v2',
12
+ 'conversation_page', 'conversation_follow', 'submit_prompt_v2', 'submit_voice_prompt', 'interrupt_v2',
12
13
  'respond_permission_v2',
13
14
  ]);
14
15
  /** The one retained-range projection shared by polling and live-follow admission. */
@@ -99,7 +100,7 @@ export function oversightTaxonomy(name = '<Name>') {
99
100
  export function oversightTaxonomyLines(name = '<Name>') {
100
101
  return oversightTaxonomy(name).map(r => `- **${r.result}** — ${r.meaning} → ${r.action}`);
101
102
  }
102
- export const controlSocketPath = (stateDir) => join(stateDir, '.control.sock');
103
+ export const controlSocketPath = (stateDir) => privateSocketPath(join(stateDir, '.control.sock'));
103
104
  export const controlTokenPath = (stateDir) => join(stateDir, '.control-token');
104
105
  function sameToken(actual, supplied) {
105
106
  const a = Buffer.from(actual);
@@ -116,9 +117,13 @@ export class RoleControlServer {
116
117
  sockets = new Set();
117
118
  /** Interrupt idempotency: same command id returns the first receipt. */
118
119
  interruptCommands = new Map();
120
+ correspondence;
121
+ setCorrespondence(value) { this.correspondence = value; }
119
122
  ownerChannel;
120
123
  loopManager;
121
124
  reloadConfig;
125
+ layoutControl;
126
+ setLayoutControl(handler) { this.layoutControl = handler; }
122
127
  fleetSpawner;
123
128
  fleetAuditor;
124
129
  constructor(stateDir, session, log) {
@@ -235,6 +240,7 @@ export class RoleControlServer {
235
240
  protocolVersion: this.session.conversationPage ? 3 : 2,
236
241
  features: [
237
242
  'events_since', 'observer_follow', 'retained_range',
243
+ ...(this.correspondence ? ['agent_correspondence'] : []),
238
244
  ...(this.session.conversationPage ? ['conversation_v3'] : []),
239
245
  ...(this.session.capabilities?.steering ? ['steering'] : []),
240
246
  ...(this.session.capabilities?.permissions ? ['permissions'] : []),
@@ -308,6 +314,13 @@ export class RoleControlServer {
308
314
  });
309
315
  return;
310
316
  }
317
+ case 'layout_control': {
318
+ if (request.version !== 2 || !request.layout || !this.layoutControl)
319
+ throw new SessionControlError('rejected', 'layout control unavailable');
320
+ const result = await this.layoutControl(request.layout);
321
+ this.write(socket, { version: 1, id: request.id, ok: true, result });
322
+ return;
323
+ }
311
324
  case 'fleet_spawn': {
312
325
  if (request.version !== 2 || !request.spawn || typeof request.spawn.name !== 'string')
313
326
  throw new SessionControlError('rejected', 'version 2 and typed spawn options are required');
@@ -362,6 +375,23 @@ export class RoleControlServer {
362
375
  this.write(socket, { version: 1, id: request.id, ok: true, result: { delivered: true } });
363
376
  return;
364
377
  }
378
+ case 'agent_contacts':
379
+ case 'agent_history': {
380
+ if (!this.correspondence)
381
+ throw new SessionControlError('rejected', 'Supervisor correspondence API is unavailable; this session needs an updated supervisor.');
382
+ const query = request.agentHistory;
383
+ if (request.command === 'agent_history' && (!query || !/^[a-f0-9]{64}$/i.test(query.peer_cid) || (query.limit !== undefined && (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > 200)) || (query.before_seq !== undefined && (!Number.isSafeInteger(query.before_seq) || query.before_seq < 1))))
384
+ throw new SessionControlError('rejected', 'Valid peer_cid, limit (1–200) and before_seq are required.');
385
+ let result;
386
+ try {
387
+ result = request.command === 'agent_contacts' ? await this.correspondence.contacts() : await this.correspondence.history(query);
388
+ }
389
+ catch {
390
+ throw new SessionControlError('rejected', 'Supervisor correspondence read failed; identity or transport is unavailable.');
391
+ }
392
+ this.write(socket, { version: 1, id: request.id, ok: true, result });
393
+ return;
394
+ }
365
395
  case 'owner_channel_manage': {
366
396
  if (!request.ownerChannel || typeof request.ownerChannel.action !== 'string')
367
397
  throw new SessionControlError('rejected', 'owner-channel management action is required');
@@ -421,7 +451,10 @@ export class RoleControlServer {
421
451
  socket.write(JSON.stringify({ version: 1, conversationEvent: event }) + '\n');
422
452
  }) };
423
453
  }
454
+ case 'submit_voice_prompt':
424
455
  case 'submit_prompt_v2': {
456
+ if (request.command === 'submit_voice_prompt' && !request.expectedSessionGeneration?.trim())
457
+ throw new SessionControlError('rejected', 'expectedSessionGeneration is required');
425
458
  this.requireConversation(request);
426
459
  if (!request.commandId?.trim() || !request.text?.trim() || !request.actor?.trim()
427
460
  || request.source !== 'owner_admin_console')
@@ -432,6 +465,7 @@ export class RoleControlServer {
432
465
  const receipt = await this.session.submitPromptBrowser({
433
466
  commandId: request.commandId, text: request.text,
434
467
  source: 'owner_admin_console', actorBrowserSession: request.actor,
468
+ expectedSessionGeneration: request.expectedSessionGeneration,
435
469
  });
436
470
  this.write(socket, { version: 1, id: request.id, ok: true, result: receipt });
437
471
  }
@@ -1,5 +1,5 @@
1
1
  import type * as acp from '@agentclientprotocol/sdk';
2
- import type { AdapterMeta, ConversationEventKind, ConversationPayload } from './conversation-types.js';
2
+ import type { AdapterMeta, BoundedJson, ConversationEventKind, ConversationPayload } from './conversation-types.js';
3
3
  /**
4
4
  * Reduce one ACP v1 `session/update` into one conversation-domain event draft.
5
5
  *
@@ -43,4 +43,5 @@ export interface NormalizedUpdate {
43
43
  toolCallId?: string;
44
44
  adapterMeta?: AdapterMeta[];
45
45
  }
46
+ export declare function boundedJson(value: unknown): BoundedJson;
46
47
  export declare function normalizeSessionUpdate(update: acp.SessionUpdate, options?: NormalizeOptions): NormalizedUpdate;
@@ -246,7 +246,7 @@ function redactSensitiveJson(value) {
246
246
  };
247
247
  return { value: visit(value, 0), redacted };
248
248
  }
249
- function boundedJson(value) {
249
+ export function boundedJson(value) {
250
250
  let serialized;
251
251
  let safe;
252
252
  let redacted = false;
@@ -1,3 +1,4 @@
1
+ import type { TaskCreatedNotice } from './task-notice.js';
1
2
  import type { PromptOrigin, TurnCancellationSource, TurnOutcome } from './types.js';
2
3
  /**
3
4
  * Durable conversation domain schema for managed-session consoles.
@@ -7,11 +8,11 @@ import type { PromptOrigin, TurnCancellationSource, TurnOutcome } from './types.
7
8
  * file touches the wire: transport updates are reduced into these shapes by a
8
9
  * session adapter, and the store assigns `seq`/`eventId`/timestamps.
9
10
  */
10
- export type ConversationEventKind = 'prompt.admitted' | 'prompt.started' | 'prompt.interrupt_requested' | 'message.chunk' | 'message.replace' | 'thought.chunk' | 'thought.replace' | 'plan.replace' | 'tool.upsert' | 'tool.content_chunk' | 'permission.requested' | 'permission.resolved' | 'monitor.delivery' | 'usage.updated' | 'turn.state' | 'turn.completed' | 'session.state' | 'session.info' | 'capabilities.updated' | 'error'
11
+ export type ConversationEventKind = 'prompt.admitted' | 'prompt.started' | 'prompt.interrupt_requested' | 'message.chunk' | 'message.replace' | 'thought.chunk' | 'thought.replace' | 'plan.replace' | 'tool.upsert' | 'tool.content_chunk' | 'permission.requested' | 'permission.resolved' | 'fleet.task_created' | 'monitor.delivery' | 'usage.updated' | 'turn.state' | 'turn.completed' | 'session.state' | 'session.info' | 'capabilities.updated' | 'error'
11
12
  /** A well-formed transport update this version cannot represent. Bounded, never a crash. */
12
13
  | 'unsupported';
13
14
  /** Where a conversation record came from. Typed provenance, never prompt text. */
14
- export type ConversationSource = 'owner_admin_console' | 'owner_channel' | 'fleet_monitor' | 'scheduled_loop' | 'startup' | 'local_console' | 'agent' | 'agent_replay';
15
+ export type ConversationSource = 'owner_admin_console' | 'owner_channel' | 'fleet_monitor' | 'scheduled_loop' | 'startup' | 'local_console' | 'agent' | 'agent_replay' | 'fleet_lifecycle';
15
16
  export interface NormalizedText {
16
17
  type: 'text';
17
18
  text: string;
@@ -227,7 +228,7 @@ export interface BoundedJson {
227
228
  digest?: string;
228
229
  redacted?: true;
229
230
  }
230
- export type ConversationPayload = MessageChunkPayload | ThoughtChunkPayload | PlanReplacePayload | ToolUpsertPayload | UsageUpdatedPayload | SessionStatePayload | SessionInfoPayload | CapabilitiesUpdatedPayload | UnsupportedPayload | PromptAdmittedPayload | PromptStartedPayload | PromptInterruptRequestedPayload | PermissionRequestedPayload | PermissionResolvedPayload | TurnStatePayload | TurnCompletedPayload | MonitorDeliveryPayload | SessionLifecyclePayload | ErrorPayload;
231
+ export type ConversationPayload = MessageChunkPayload | ThoughtChunkPayload | PlanReplacePayload | ToolUpsertPayload | UsageUpdatedPayload | SessionStatePayload | SessionInfoPayload | CapabilitiesUpdatedPayload | UnsupportedPayload | PromptAdmittedPayload | PromptStartedPayload | PromptInterruptRequestedPayload | PermissionRequestedPayload | PermissionResolvedPayload | TurnStatePayload | TurnCompletedPayload | MonitorDeliveryPayload | SessionLifecyclePayload | ErrorPayload | TaskCreatedNotice;
231
232
  export interface ConversationEventV1 {
232
233
  schemaVersion: 1;
233
234
  roleId: string;
@@ -262,6 +263,7 @@ export interface ConversationEventV1 {
262
263
  adapterMeta?: AdapterMeta[];
263
264
  }
264
265
  export interface SubmitPromptCommand {
266
+ expectedSessionGeneration?: string;
265
267
  /** Idempotency-Key / clientRequestId. Reuse with a different body is a conflict. */
266
268
  commandId: string;
267
269
  text: string;
@@ -0,0 +1,14 @@
1
+ import type { ConversationEventStore } from './conversation-store.js';
2
+ export interface TaskNoticeBinding {
3
+ sessionGeneration: string;
4
+ acpSessionId: string;
5
+ promptId: string;
6
+ }
7
+ export interface TaskCreatedNotice {
8
+ operationId: string;
9
+ taskId: string;
10
+ title: string;
11
+ state: string;
12
+ }
13
+ /** Only the trusted Fleet audit completion calls this, never model output. */
14
+ export declare function appendTaskCreated(store: ConversationEventStore, binding: TaskNoticeBinding, notice: TaskCreatedNotice): void;
@@ -0,0 +1,13 @@
1
+ /** Only the trusted Fleet audit completion calls this, never model output. */
2
+ export function appendTaskCreated(store, binding, notice) {
3
+ let after;
4
+ do {
5
+ const page = store.page({ after, limit: 1000 });
6
+ if (page.events.some(e => e.kind === 'fleet.task_created' && e.payload.operationId === notice.operationId))
7
+ return;
8
+ if (!page.hasMore)
9
+ break;
10
+ after = page.nextCursor;
11
+ } while (after);
12
+ store.append({ kind: 'fleet.task_created', source: 'fleet_lifecycle', ...binding, turnId: binding.promptId, payload: notice });
13
+ }
@@ -1,3 +1,4 @@
1
+ import type { TaskCreatedNotice, TaskNoticeBinding } from './task-notice.js';
1
2
  import type { SessionBackendId } from '../config.js';
2
3
  import type { ConversationEventV1, ConversationSnapshot, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
3
4
  /**
@@ -289,6 +290,8 @@ export interface AgentSession {
289
290
  readonly capabilities?: AgentSessionCapabilities;
290
291
  isAlive(): boolean;
291
292
  snapshot(): SessionSnapshot;
293
+ taskNoticeBinding?(): TaskNoticeBinding | undefined;
294
+ recordTaskCreated?(binding: TaskNoticeBinding, notice: TaskCreatedNotice): void;
292
295
  conversationPage?(request: {
293
296
  after?: string;
294
297
  limit?: number;
@@ -0,0 +1,2 @@
1
+ /** Optional private socket directory for deeply nested, isolated Fleet homes. */
2
+ export declare function socketPath(logicalPath: string): string;
@@ -0,0 +1,22 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstatSync, mkdirSync } from 'node:fs';
3
+ import { isAbsolute, join, resolve } from 'node:path';
4
+ /** Optional private socket directory for deeply nested, isolated Fleet homes. */
5
+ export function socketPath(logicalPath) {
6
+ const root = process.env.OURS_FLEET_SOCKET_ROOT;
7
+ if (!root) {
8
+ if (Buffer.byteLength(logicalPath) > 103)
9
+ throw new Error('Unix socket path is too long; configure a short private OURS_FLEET_SOCKET_ROOT');
10
+ return logicalPath;
11
+ }
12
+ if (!isAbsolute(root) || resolve(root) !== root)
13
+ throw new Error('OURS_FLEET_SOCKET_ROOT must be an absolute normalized path');
14
+ mkdirSync(root, { recursive: true, mode: 0o700 });
15
+ const stat = lstatSync(root);
16
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o700)
17
+ throw new Error('OURS_FLEET_SOCKET_ROOT must be an owner-only 0700 directory');
18
+ const path = join(root, createHash('sha256').update(resolve(logicalPath)).digest('hex').slice(0, 24) + '.sock');
19
+ if (Buffer.byteLength(path) > 103)
20
+ throw new Error('OURS_FLEET_SOCKET_ROOT is too long');
21
+ return path;
22
+ }
package/dist/spawn.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { storeRoomSecret, storeTemporaryLaunch, preparePermanentAssignment } from './agent-ours/service.js';
3
+ import { ensureWorkspace } from './rooms-tasks/workspace.js';
1
4
  import { spawn as spawnChild } from 'node:child_process';
2
5
  import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
6
  import { join } from 'node:path';
@@ -7,7 +10,7 @@ import { validateIsolationConfig } from './isolation/policy.js';
7
10
  import { loadConfig, findRole, splitRootFor, validateMonitorConfig, } from './config.js';
8
11
  import { applyRole, up } from './ops.js';
9
12
  import { START_STAGGER_FILE } from './runner.js';
10
- import { buildProvenance, daemonIdentityProvisioner, ensureIdentity, provenanceOf, withCreationTransaction, writeProvenance, writeRoleFile, } from './creation.js';
13
+ import { buildProvenance, provenanceOf, withCreationTransaction, writeProvenance, writeRoleFile, } from './creation.js';
11
14
  import { VERSION } from './version.js';
12
15
  import { recordGeneratedAgentSource } from './generated-agent-source.js';
13
16
  import './harness/claude-code.js';
@@ -239,22 +242,12 @@ export async function spawnPermanent(o, deps, creation = {}) {
239
242
  return withCreationTransaction({ role: o.name, identity: effectiveIdentity(o) }, async (tx) => {
240
243
  assertNameFree(o);
241
244
  const cfg = loadConfig(o.configPath);
242
- // Establish the identity BEFORE the service is enabled, and record
243
- // what was actually guaranteed so the briefing can say something true.
245
+ // Pin an existing assignment without binding; the supervisor provisions
246
+ // a missing role identity before starting its harness.
244
247
  creation.onStage?.('checking_identity');
245
- const guarantee = await ensureIdentity(effectiveIdentity(o), { bio: prepared.role.bio, persona: prepared.role.persona }, creation.identityProvisioner ?? deps.identityProvisioner ?? daemonIdentityProvisioner(), deps.log);
246
- creation.onStage?.('checking_identity', {
247
- result: guarantee.evidence, guarantee: guarantee.state,
248
- });
249
- if (guarantee.state === 'created')
250
- // We minted it; a failed creation must not leave an orphan identity
251
- // behind. Only ever removes an identity THIS transaction created.
252
- tx.record({
253
- stage: `ours identity ${effectiveIdentity(o)}`,
254
- undo: async () => {
255
- await creation.identityProvisioner?.remove?.(effectiveIdentity(o));
256
- },
257
- });
248
+ const assignment = await preparePermanentAssignment(prepared.role);
249
+ const guarantee = { state: assignment, evidence: assignment === 'verified' ? 'verified' : 'missing' };
250
+ creation.onStage?.('checking_identity', { result: guarantee.evidence, guarantee: guarantee.state });
258
251
  const agentRoot = join(splitRootFor(o.configPath ?? defaultConfigPath()), 'agents');
259
252
  mkdirSync(agentRoot, { recursive: true });
260
253
  creation.onStage?.('writing_role');
@@ -329,6 +322,12 @@ export async function spawnTemp(o, binPath, launch = independentSupervisor, crea
329
322
  if (o.isolationFile)
330
323
  readIsolationFile(o.isolationFile); // fail before reserving
331
324
  const prepared = resolvedSpawn(o); // canonical validation before mutation
325
+ if (o.roomMemberStartup?.workspace) {
326
+ const workspace = o.roomMemberStartup.workspace;
327
+ if (prepared.role.cwd !== workspace.path)
328
+ throw new Error('room member cwd must equal its recorded owned workspace');
329
+ ensureWorkspace(workspace);
330
+ }
332
331
  // Retire only supervisors whose recorded owner is definitively stopped. This
333
332
  // bounded pass keeps the active roster clean without deleting old evidence.
334
333
  await reclaimStaleTempState();
@@ -354,6 +353,10 @@ async function spawnTempInner(o, preparedRole, binPath, launch, tx, onStage) {
354
353
  temporaryLoopSource: o.loopSource ?? (temporaryLoops?.length ? 'agent-template' : 'omitted'),
355
354
  roomMemberStartup: o.roomMemberStartup,
356
355
  };
356
+ storeTemporaryLaunch(role, o.creationActionId ?? randomUUID());
357
+ storeRoomSecret(role);
358
+ if (role.roomMemberStartup)
359
+ role.roomMemberStartup = { ...role.roomMemberStartup, invite: '' };
357
360
  onStage?.('writing_role');
358
361
  const dir = applyRole(role, { temp: true, identityGuarantee: 'unverified' });
359
362
  const provenance = buildProvenance({
@@ -0,0 +1,13 @@
1
+ import { type DaemonGenerationProbe } from './daemon-recovery.js';
2
+ export declare const STARTUP_WAIT_MS = 240000;
3
+ export declare const STARTUP_RETRY_MS = 5000;
4
+ interface ReadinessDeps {
5
+ probe(env: NodeJS.ProcessEnv): Promise<DaemonGenerationProbe>;
6
+ now(): number;
7
+ sleep(ms: number): Promise<void>;
8
+ log(message: string): void;
9
+ env: NodeJS.ProcessEnv;
10
+ }
11
+ /** Read-only preflight: never binds identities, starts a harness or edits its ledger. */
12
+ export declare function waitForRoleDaemon(name: string, configPath?: string, partial?: Partial<ReadinessDeps>): Promise<void>;
13
+ export {};
@@ -0,0 +1,38 @@
1
+ import { agentDir } from './paths.js';
2
+ import { findRole, loadConfig } from './config.js';
3
+ import { resolveConfigPath } from './runner.js';
4
+ import { probeDaemonGeneration } from './daemon-recovery.js';
5
+ export const STARTUP_WAIT_MS = 240_000;
6
+ export const STARTUP_RETRY_MS = 5_000;
7
+ /** Read-only preflight: never binds identities, starts a harness or edits its ledger. */
8
+ export async function waitForRoleDaemon(name, configPath, partial = {}) {
9
+ const deps = {
10
+ probe: env => probeDaemonGeneration(((url, init) => globalThis.fetch(url, {
11
+ ...init, signal: AbortSignal.timeout(10_000),
12
+ })), env),
13
+ now: Date.now,
14
+ sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
15
+ log: message => process.stderr.write(message + '\n'),
16
+ env: process.env,
17
+ ...partial,
18
+ };
19
+ const path = resolveConfigPath(agentDir(name), configPath);
20
+ const role = findRole(loadConfig(path), name);
21
+ const env = { ...deps.env, ...role.env };
22
+ const deadline = deps.now() + STARTUP_WAIT_MS;
23
+ let lastReason = '';
24
+ while (deps.now() < deadline) {
25
+ const result = await deps.probe(env);
26
+ if (result.state === 'ready') {
27
+ deps.log(`[${name}] daemon ready; starting supervised agent`);
28
+ return;
29
+ }
30
+ // Only structured diagnostic codes, never endpoint responses or credentials.
31
+ const reason = /^[A-Z][A-Z0-9_]*$/.test(result.reason) ? result.reason : 'DAEMON_UNAVAILABLE';
32
+ if (reason !== lastReason)
33
+ deps.log(`[${name}] waiting for configured daemon: ${reason}`);
34
+ lastReason = reason;
35
+ await deps.sleep(Math.min(STARTUP_RETRY_MS, Math.max(0, deadline - deps.now())));
36
+ }
37
+ throw new Error(`Daemon not ready (${lastReason}); service manager will retry startup`);
38
+ }
@@ -1,3 +1,4 @@
1
+ import { clientConfigPath } from '../client-profile.js';
1
2
  import { existsSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
2
3
  import { join } from 'node:path';
3
4
  import { home, logsRoot } from '../paths.js';
@@ -22,7 +23,7 @@ const xml = (value) => value
22
23
  .replaceAll("'", '&apos;');
23
24
  function plist(name, binPath) {
24
25
  const log = join(logsRoot(), `${name}.log`);
25
- const config = process.env.OURS_CONFIG;
26
+ const config = clientConfigPath(process.env);
26
27
  return `<?xml version="1.0" encoding="UTF-8"?>
27
28
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
28
29
  <plist version="1.0">