@ours.network/fleet 1.2.0-nightly.5 → 1.2.0-nightly.51

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 (330) hide show
  1. package/README.md +263 -53
  2. package/dist/account-origin.d.ts +2 -0
  3. package/dist/account-origin.js +6 -0
  4. package/dist/agent-ours/bridge.js +9 -0
  5. package/dist/agent-ours/correspondence.d.ts +10 -0
  6. package/dist/agent-ours/correspondence.js +1 -0
  7. package/dist/agent-ours/harness.js +8 -0
  8. package/dist/agent-ours/runtime.d.ts +6 -0
  9. package/dist/agent-ours/runtime.js +36 -0
  10. package/dist/agent-ours/service.js +33 -13
  11. package/dist/application/codex-models.d.ts +15 -0
  12. package/dist/application/codex-models.js +56 -0
  13. package/dist/application/conversation-history.d.ts +25 -0
  14. package/dist/application/conversation-history.js +189 -0
  15. package/dist/application/fleet-query-service.d.ts +1 -1
  16. package/dist/application/fleet-query-service.js +2 -2
  17. package/dist/application/legacy-supervisor-identity.d.ts +7 -0
  18. package/dist/application/legacy-supervisor-identity.js +43 -0
  19. package/dist/application/model-catalog.d.ts +1 -1
  20. package/dist/application/model-catalog.js +5 -2
  21. package/dist/application/preset-provenance.d.ts +19 -0
  22. package/dist/application/preset-provenance.js +59 -0
  23. package/dist/application/role-creation-service.js +1 -1
  24. package/dist/application/role-removal-service.js +2 -0
  25. package/dist/application/role-repository.js +2 -2
  26. package/dist/application/room-layout-definitions.d.ts +45 -0
  27. package/dist/application/room-layout-definitions.js +130 -0
  28. package/dist/application/session-control.d.ts +30 -0
  29. package/dist/application/session-control.js +72 -3
  30. package/dist/application/supervisor-ours-tools.d.ts +159 -0
  31. package/dist/application/supervisor-ours-tools.js +113 -0
  32. package/dist/application/task-layouts.d.ts +91 -0
  33. package/dist/application/task-layouts.js +249 -0
  34. package/dist/application/task-room-service.d.ts +99 -3
  35. package/dist/application/task-room-service.js +296 -31
  36. package/dist/atomic-file.d.ts +5 -0
  37. package/dist/atomic-file.js +62 -1
  38. package/dist/briefing.d.ts +2 -0
  39. package/dist/briefing.js +8 -1
  40. package/dist/build-info.json +7 -4
  41. package/dist/capabilities.d.ts +6 -1
  42. package/dist/capabilities.js +8 -0
  43. package/dist/cli.js +376 -34
  44. package/dist/client-profile.d.ts +5 -17
  45. package/dist/client-profile.js +4 -127
  46. package/dist/config.d.ts +28 -0
  47. package/dist/config.js +41 -6
  48. package/dist/creation.js +2 -2
  49. package/dist/daemon-recovery.d.ts +1 -8
  50. package/dist/daemon-recovery.js +8 -113
  51. package/dist/docs.d.ts +3 -3
  52. package/dist/docs.js +126 -22
  53. package/dist/doctor.d.ts +1 -1
  54. package/dist/doctor.js +55 -60
  55. package/dist/erased-resources.d.ts +9 -0
  56. package/dist/erased-resources.js +87 -0
  57. package/dist/fleet-command-audit.d.ts +7 -0
  58. package/dist/fleet-command-audit.js +26 -9
  59. package/dist/harness/claude-code.js +9 -1
  60. package/dist/harness/claude-runtime.d.ts +12 -0
  61. package/dist/harness/claude-runtime.js +43 -0
  62. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  63. package/dist/harness/codex-app-server-proxy.js +41 -5
  64. package/dist/harness/codex-reasoning.d.ts +5 -0
  65. package/dist/harness/codex-reasoning.js +12 -0
  66. package/dist/harness/codex-runtime.d.ts +11 -1
  67. package/dist/harness/codex-runtime.js +31 -4
  68. package/dist/harness/codex.d.ts +3 -2
  69. package/dist/harness/codex.js +20 -11
  70. package/dist/init-wizard.d.ts +2 -0
  71. package/dist/init-wizard.js +38 -19
  72. package/dist/isolation/policy.js +2 -0
  73. package/dist/isolation/types.d.ts +5 -0
  74. package/dist/managed-cli.d.ts +253 -0
  75. package/dist/managed-cli.js +923 -0
  76. package/dist/minimal-setup.d.ts +2 -0
  77. package/dist/minimal-setup.js +44 -0
  78. package/dist/monitor.d.ts +1 -34
  79. package/dist/monitor.js +24 -151
  80. package/dist/notifications/fleet-producer.d.ts +19 -0
  81. package/dist/notifications/fleet-producer.js +208 -0
  82. package/dist/notifications/outbox.d.ts +46 -0
  83. package/dist/notifications/outbox.js +158 -0
  84. package/dist/notifications/target-cleanup.d.ts +8 -0
  85. package/dist/notifications/target-cleanup.js +41 -0
  86. package/dist/ops.js +5 -1
  87. package/dist/owner-channel/channel.d.ts +3 -0
  88. package/dist/owner-channel/channel.js +34 -6
  89. package/dist/owner-channel/commands.d.ts +4 -0
  90. package/dist/owner-channel/commands.js +8 -0
  91. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  92. package/dist/owner-channel/ours-client.js +2 -17
  93. package/dist/paths.d.ts +2 -0
  94. package/dist/paths.js +2 -0
  95. package/dist/preset-migration.js +25 -0
  96. package/dist/resolved-plan.js +1 -0
  97. package/dist/rooms-tasks/add-member.d.ts +48 -0
  98. package/dist/rooms-tasks/add-member.js +83 -0
  99. package/dist/rooms-tasks/archived-absence.d.ts +8 -0
  100. package/dist/rooms-tasks/archived-absence.js +48 -0
  101. package/dist/rooms-tasks/cli.js +34 -5
  102. package/dist/rooms-tasks/close.d.ts +5 -2
  103. package/dist/rooms-tasks/close.js +208 -28
  104. package/dist/rooms-tasks/config.d.ts +1 -1
  105. package/dist/rooms-tasks/config.js +11 -4
  106. package/dist/rooms-tasks/cowork-adapter.d.ts +6 -4
  107. package/dist/rooms-tasks/cowork-adapter.js +8 -121
  108. package/dist/rooms-tasks/deletion.d.ts +1 -1
  109. package/dist/rooms-tasks/deletion.js +74 -5
  110. package/dist/rooms-tasks/erasure.d.ts +6 -0
  111. package/dist/rooms-tasks/erasure.js +169 -0
  112. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  113. package/dist/rooms-tasks/external-worker.js +3 -4
  114. package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
  115. package/dist/rooms-tasks/layout-binding-client.js +56 -0
  116. package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
  117. package/dist/rooms-tasks/layout-binding-grants.js +128 -0
  118. package/dist/rooms-tasks/layout-cli.d.ts +4 -0
  119. package/dist/rooms-tasks/layout-cli.js +107 -0
  120. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  121. package/dist/rooms-tasks/layout-config.js +89 -0
  122. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  123. package/dist/rooms-tasks/layout-control.js +52 -0
  124. package/dist/rooms-tasks/layout-service.d.ts +42 -0
  125. package/dist/rooms-tasks/layout-service.js +185 -0
  126. package/dist/rooms-tasks/layout.d.ts +114 -0
  127. package/dist/rooms-tasks/layout.js +250 -0
  128. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  129. package/dist/rooms-tasks/live-readiness.js +85 -0
  130. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  131. package/dist/rooms-tasks/member-startup.js +2 -0
  132. package/dist/rooms-tasks/provision.d.ts +14 -0
  133. package/dist/rooms-tasks/provision.js +173 -46
  134. package/dist/rooms-tasks/room-state.d.ts +4 -2
  135. package/dist/rooms-tasks/room-state.js +53 -8
  136. package/dist/rooms-tasks/task-state.d.ts +42 -7
  137. package/dist/rooms-tasks/task-state.js +153 -23
  138. package/dist/rooms-tasks/types.d.ts +28 -0
  139. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  140. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  141. package/dist/rooms-tasks/workspace.d.ts +26 -0
  142. package/dist/rooms-tasks/workspace.js +195 -0
  143. package/dist/runner.d.ts +19 -3
  144. package/dist/runner.js +128 -45
  145. package/dist/session/acp.d.ts +9 -1
  146. package/dist/session/acp.js +31 -6
  147. package/dist/session/arbiter.d.ts +5 -0
  148. package/dist/session/arbiter.js +28 -2
  149. package/dist/session/codex-app-server.d.ts +5 -0
  150. package/dist/session/codex-app-server.js +36 -3
  151. package/dist/session/control.d.ts +12 -1
  152. package/dist/session/control.js +68 -3
  153. package/dist/session/conversation-normalizer.d.ts +2 -1
  154. package/dist/session/conversation-normalizer.js +1 -1
  155. package/dist/session/conversation-store.d.ts +1 -1
  156. package/dist/session/conversation-store.js +7 -11
  157. package/dist/session/conversation-types.d.ts +7 -3
  158. package/dist/session/task-notice.d.ts +14 -0
  159. package/dist/session/task-notice.js +13 -0
  160. package/dist/session/types.d.ts +6 -0
  161. package/dist/setup-tunnel-preflight.d.ts +34 -0
  162. package/dist/setup-tunnel-preflight.js +62 -0
  163. package/dist/socket-path.d.ts +2 -0
  164. package/dist/socket-path.js +22 -0
  165. package/dist/spawn.js +8 -1
  166. package/dist/startup-readiness.d.ts +13 -0
  167. package/dist/startup-readiness.js +38 -0
  168. package/dist/subscriptions/cli.d.ts +91 -0
  169. package/dist/subscriptions/cli.js +223 -0
  170. package/dist/subscriptions/launch.d.ts +37 -0
  171. package/dist/subscriptions/launch.js +93 -0
  172. package/dist/subscriptions/login.d.ts +75 -0
  173. package/dist/subscriptions/login.js +182 -0
  174. package/dist/subscriptions/observed.d.ts +7 -0
  175. package/dist/subscriptions/observed.js +44 -0
  176. package/dist/subscriptions/probe.d.ts +10 -0
  177. package/dist/subscriptions/probe.js +73 -0
  178. package/dist/subscriptions/service.d.ts +102 -0
  179. package/dist/subscriptions/service.js +364 -0
  180. package/dist/subscriptions/store.d.ts +71 -0
  181. package/dist/subscriptions/store.js +140 -0
  182. package/dist/supervisor/launchd.js +2 -1
  183. package/dist/supervisor/systemd.js +9 -23
  184. package/dist/temp-idle.d.ts +17 -0
  185. package/dist/temp-idle.js +42 -0
  186. package/dist/temp-lifecycle.d.ts +6 -1
  187. package/dist/temp-lifecycle.js +29 -13
  188. package/dist/tunnel-setup-completion.d.ts +7 -0
  189. package/dist/tunnel-setup-completion.js +13 -0
  190. package/dist/tunnel-setup-input.d.ts +19 -0
  191. package/dist/tunnel-setup-input.js +124 -0
  192. package/dist/web/agent-attachments.d.ts +29 -0
  193. package/dist/web/agent-attachments.js +162 -0
  194. package/dist/web/auth.d.ts +29 -1
  195. package/dist/web/auth.js +82 -7
  196. package/dist/web/available-port.d.ts +2 -0
  197. package/dist/web/available-port.js +23 -0
  198. package/dist/web/control.d.ts +3 -3
  199. package/dist/web/control.js +10 -8
  200. package/dist/web/fleet-config-service.d.ts +4 -1
  201. package/dist/web/fleet-config-service.js +46 -15
  202. package/dist/web/prefix-gateway.d.ts +32 -0
  203. package/dist/web/prefix-gateway.js +234 -0
  204. package/dist/web/runtime.d.ts +3 -1
  205. package/dist/web/runtime.js +116 -31
  206. package/dist/web/server.d.ts +16 -0
  207. package/dist/web/server.js +592 -53
  208. package/dist/web/service.d.ts +11 -2
  209. package/dist/web/service.js +30 -4
  210. package/dist/web/workspace-devices.d.ts +36 -0
  211. package/dist/web/workspace-devices.js +101 -0
  212. package/dist/web/workspace-tunnel.d.ts +1 -0
  213. package/dist/web/workspace-tunnel.js +34 -0
  214. package/dist/workspace-enrollment.d.ts +82 -0
  215. package/dist/workspace-enrollment.js +566 -0
  216. package/dist/workspace-replacement.d.ts +66 -0
  217. package/dist/workspace-replacement.js +220 -0
  218. package/docs/live-voice.md +13 -0
  219. package/docs/room-layouts.md +227 -0
  220. package/docs/task-workspaces.md +130 -0
  221. package/docs/validation/managed-cli-permissions.md +421 -0
  222. package/examples/room-layouts/fresh.yaml +27 -0
  223. package/examples/room-layouts/scoped.yaml +21 -0
  224. package/examples/room-layouts/shared.yaml +19 -0
  225. package/package.json +15 -18
  226. package/presets/README.md +78 -0
  227. package/presets/brain-catalog.json +8 -2
  228. package/presets/fleet/agent_templates/Critic.yaml +1 -1
  229. package/presets/fleet/agent_templates/Developer.yaml +1 -1
  230. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  231. package/presets/fleet/agent_templates/LocalCoordinator.yaml +1 -1
  232. package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
  233. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  234. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  235. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  236. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  237. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  238. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  239. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  240. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  241. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  242. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  243. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  244. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  245. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  246. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  247. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  248. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  249. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  250. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  251. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  252. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  253. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  254. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  255. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  256. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  257. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  258. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  259. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  260. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  261. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  262. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  263. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  264. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  265. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  266. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  267. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  268. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  269. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  270. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  271. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  272. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  273. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  274. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  275. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  276. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  277. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  278. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  279. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  280. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  281. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  282. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  283. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  284. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  285. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  286. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  287. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  288. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  289. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  290. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  291. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  292. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  293. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  294. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  295. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  296. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  297. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  298. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  299. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  300. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  301. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  302. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  303. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  304. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  305. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  306. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  307. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  308. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  309. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  310. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  311. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  312. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  313. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  314. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  315. package/presets/fleet/roles/Coordinator.yaml +34 -11
  316. package/presets/fleet/roles/Engineer.yaml +40 -0
  317. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  318. package/presets/fleet/room_layouts/pair.yaml +24 -0
  319. package/presets/fleet/room_layouts/single.yaml +17 -0
  320. package/presets/fleet/room_layouts/team.yaml +30 -0
  321. package/presets/fleet/room_templates/engineering.yaml +19 -0
  322. package/presets/manifest.json +2 -2
  323. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  324. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  325. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  326. package/dist/web-app/icons/ours-fleet.svg +0 -4
  327. package/dist/web-app/index.html +0 -17
  328. package/dist/web-app/manifest.webmanifest +0 -15
  329. package/dist/web-app/offline.html +0 -18
  330. package/dist/web-app/sw.js +0 -51
package/dist/runner.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { prepareManagedAgent, releaseManagedAgent } from './agent-ours/service.js';
2
2
  import { type ResolvedRole } from './config.js';
3
- import { type MonitorHandle, type MonitorOpts, type FetchLike } from './monitor.js';
3
+ import { probeIdentityPresence, type MonitorHandle, type MonitorOpts, type FetchLike } from './monitor.js';
4
4
  import { type DaemonGenerationProbe } from './daemon-recovery.js';
5
5
  import { type Exec } from './exec.js';
6
6
  import { RoleControlServer } from './session/control.js';
@@ -20,6 +20,7 @@ export interface RunnerDeps {
20
20
  log(line: string): void;
21
21
  /** HTTP transport for the monitor's daemon long-poll (injectable for tests). */
22
22
  fetch: FetchLike;
23
+ identityProbeDeps?: Parameters<typeof probeIdentityPresence>[3];
23
24
  probeGeneration(env: NodeJS.ProcessEnv): Promise<DaemonGenerationProbe>;
24
25
  /** Construct the supervisor mail monitor (injectable so tests stub it out). */
25
26
  createMonitor(opts: MonitorOpts): MonitorHandle;
@@ -29,7 +30,7 @@ export interface RunnerDeps {
29
30
  /** Neutral construction seam for deterministic runner lifecycle tests. */
30
31
  startAgentSession(adapter: AgentSessionAdapter, options: AgentSessionStartOptions): Promise<AgentSession>;
31
32
  /** Construct the authenticated role control route (injectable where sockets are unavailable). */
32
- createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setFleetAuditor' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'>;
33
+ createControlServer(stateDir: string, session: AgentSession, log: (line: string) => void): Pick<RoleControlServer, 'start' | 'close' | 'setFleetSpawner' | 'setFleetAuditor' | 'setOwnerChannel' | 'setConfigReloader' | 'setLoopManager'> & Partial<Pick<RoleControlServer, 'setLayoutControl' | 'setCorrespondence'>>;
33
34
  /** Construct scheduled-loop execution (injectable for fail-closed startup tests). */
34
35
  createLoopManager(...args: ConstructorParameters<typeof ScheduledLoopManager>): ScheduledLoopManagerHandle;
35
36
  /** Acquire the cross-process owner-channel binder lease before replacing the control socket. */
@@ -44,6 +45,11 @@ export declare class SupervisorRecycleRequiredError extends Error {
44
45
  readonly code = "OWNER_CHANNEL_SUPERVISOR_RECYCLE_REQUIRED";
45
46
  constructor();
46
47
  }
48
+ export declare function localFleetAuditor(stateDir: string, caller: string, log: (line: string) => void, session: AgentSession): {
49
+ begin(requestId: string, argv: string[]): Promise<import("./fleet-command-audit.js").FleetAuditAttempt>;
50
+ finish(input: Parameters<NonNullable<OwnerChannelHandle["finishFleetCommandAudit"]>>[0]): Promise<import("./fleet-command-audit.js").FleetAuditAttempt>;
51
+ present(): Promise<void>;
52
+ };
47
53
  /** Environment injected only into the managed harness process. */
48
54
  export declare function managedFleetProxyEnv(role: ResolvedRole, stateDir: string): Record<string, string>;
49
55
  /**
@@ -149,6 +155,16 @@ export declare const START_STAGGER_FILE = ".start-stagger-ms";
149
155
  export declare function reserveLaunchSlot(root: string, staggerMs: number, deps: Pick<RunnerDeps, 'now' | 'sleep' | 'log'>): Promise<number>;
150
156
  /** Read a temp role's config snapshot written by spawnTemp. */
151
157
  export declare function loadTempRole(name: string): ResolvedRole;
158
+ /**
159
+ * Fall back to the config path applyRole() recorded at the last up/restart/spawn
160
+ * for this role. systemd's shared unit template (`ours-fleet-agent@.service`)
161
+ * execs `_run <name>` with no -c on every restart, so a role brought up from a
162
+ * non-default config (`ours-fleet up -c custom.yaml`) would otherwise silently
163
+ * resolve against the default ~/fleet.yaml on its very first restart and fail
164
+ * with "no such role". An empty/missing marker means "use the default", same as
165
+ * no -c was ever given.
166
+ */
167
+ export declare function resolveConfigPath(dir: string, explicit?: string): string | undefined;
152
168
  /** What one child session did, so the supervising loop can decide what follows. */
153
169
  export interface AttemptResult {
154
170
  elapsedSecs: number;
@@ -158,7 +174,7 @@ export interface AttemptResult {
158
174
  mode: 'fresh' | 'resume';
159
175
  modelRecovery?: 'advance' | 'hold';
160
176
  /** Present only when a temporary-role lifecycle signal ended the session. */
161
- retirementReason?: 'identity-closed' | 'operator-stop' | 'supervisor-signal';
177
+ retirementReason?: 'identity-closed' | 'operator-stop' | 'supervisor-signal' | 'idle-timeout';
162
178
  }
163
179
  /** Continuous authoritative absence required after an identity was observed. */
164
180
  export declare const TEMP_IDENTITY_CLOSE_DEBOUNCE_MS = 5000;
package/dist/runner.js CHANGED
@@ -1,17 +1,21 @@
1
+ import { createLayoutControl } from './rooms-tasks/layout-control.js';
2
+ import { TemporaryChatIdle } from './temp-idle.js';
1
3
  import { prepareManagedAgent, releaseManagedAgent } from './agent-ours/service.js';
2
4
  import { prepareManagedHarness } from './agent-ours/harness.js';
5
+ import { prepareManagedCliLaunch } from './managed-cli.js';
3
6
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, realpathSync } from 'node:fs';
4
7
  import { join } from 'node:path';
5
8
  import { fileURLToPath } from 'node:url';
6
9
  import { randomUUID } from 'node:crypto';
7
10
  import { parse } from 'yaml';
8
11
  import { agentDir, stateRoot } from './paths.js';
9
- import { loadConfig, findRole, isolationContextFor, resolveMonitorConfig, resolvePermissions, } from './config.js';
12
+ import { loadConfig, findRole, isolationContextFor, resolveSnapshotMonitor, resolvePermissions, } from './config.js';
10
13
  import { getAdapter } from './harness/registry.js';
11
14
  import { createMonitor, probeIdentityPresence, } from './monitor.js';
12
15
  import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, } from './daemon-recovery.js';
13
16
  import { realExec } from './exec.js';
14
17
  import { resolveIsolation } from './isolation/policy.js';
18
+ import { pinSubscriptionForLaunch } from './subscriptions/launch.js';
15
19
  import { selectIsolationBackend } from './isolation/registry.js';
16
20
  import { resourceArgs, cpuControllerDelegated } from './isolation/resources.js';
17
21
  import { resolveLaunchRuntime } from './isolation/runtime.js';
@@ -75,12 +79,16 @@ const defaultDeps = () => ({
75
79
  });
76
80
  const MONITOR_OWNER_FILE = '.monitor-owner';
77
81
  const OBSOLETE_OURS_AUTOSTART_ENV = 'OURS_AUTOSTART';
78
- function localFleetAuditor(stateDir, caller, log) {
82
+ export function localFleetAuditor(stateDir, caller, log, session) {
83
+ const bindings = new Map();
79
84
  const store = new FleetCommandAuditStore(join(stateDir, '.fleet-command-audit.json'));
80
85
  return {
81
86
  async begin(requestId, argv) {
82
87
  let attempt = store.begin(requestId, caller, argv);
83
88
  if (attempt.invocation === 'sending') {
89
+ const binding = session.taskNoticeBinding?.();
90
+ if (binding)
91
+ bindings.set(attempt.correlationId, binding);
84
92
  log(`[${caller}] fleet proxy command ${attempt.correlationId} `
85
93
  + `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
86
94
  attempt = store.invocation(attempt.correlationId, caller, 'delivered');
@@ -94,6 +102,19 @@ function localFleetAuditor(stateDir, caller, log) {
94
102
  ...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
95
103
  ...(input.presentations ? { presentations: input.presentations } : {}),
96
104
  });
105
+ const binding = bindings.get(attempt.correlationId);
106
+ if (binding && attempt.outcome?.class === 'success' && attempt.outcome.effect === 'completed') {
107
+ for (const p of attempt.outcome.presentations ?? [])
108
+ if (p.kind === 'task' && p.operation === 'create') {
109
+ try {
110
+ session.recordTaskCreated?.(binding, { operationId: p.eventId, taskId: p.id, title: p.title ?? p.id, state: p.newState });
111
+ }
112
+ catch {
113
+ log(`[${caller}] task-created conversation notice could not be persisted`);
114
+ }
115
+ }
116
+ bindings.delete(attempt.correlationId);
117
+ }
97
118
  if (attempt.outcome?.delivery === 'sending')
98
119
  attempt = store.outcome(attempt.correlationId, caller, 'delivered');
99
120
  return attempt;
@@ -414,7 +435,7 @@ export function loadTempRole(name) {
414
435
  // Upgrade snapshots written before monitor.mode/interrupt existed. A snapshot
415
436
  // with no monitor block keeps the historical native/no-supervisor behavior.
416
437
  if (role.monitor)
417
- role.monitor = resolveMonitorConfig(undefined, role.monitor);
438
+ role.monitor = resolveSnapshotMonitor(role.monitor);
418
439
  role.__temp = true;
419
440
  return role;
420
441
  }
@@ -427,7 +448,7 @@ export function loadTempRole(name) {
427
448
  * with "no such role". An empty/missing marker means "use the default", same as
428
449
  * no -c was ever given.
429
450
  */
430
- function resolveConfigPath(dir, explicit) {
451
+ export function resolveConfigPath(dir, explicit) {
431
452
  if (explicit)
432
453
  return explicit;
433
454
  const marker = join(dir, '.config-path');
@@ -502,10 +523,22 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
502
523
  writeFileSync(bootedFile, `${new Date(deps.now()).toISOString()} ${mode}\n`);
503
524
  const runCwd = role.cwd && existsSync(role.cwd) ? role.cwd : dir;
504
525
  role = { ...role, monitor: { ...role.monitor, mode: 'fleet' } };
505
- const prep = await adapter.prepareSession(role, { stateDir: dir, runCwd });
526
+ // Native policy for Fleet commands run from the harness command sandbox. This
527
+ // is the same step for configured, spawned and task-room agents alike.
528
+ const prep = prepareManagedCliLaunch(role, {
529
+ // A temporary agent has no config path threaded through its detached
530
+ // supervisor; spawn records the one it was created from beside its snapshot.
531
+ stateDir: dir, runCwd, configPath: configPath ?? resolveConfigPath(dir), temp,
532
+ prep: await adapter.prepareSession(role, { stateDir: dir, runCwd }), log: deps.log,
533
+ });
506
534
  const sessionBackend = role.session ?? 'acp';
507
535
  const sessionLabel = sessionBackend === 'acp' ? 'ACP' : 'Codex app-server';
508
536
  let launch = adapter.agentSession.prepareLaunch(role, prep);
537
+ // Resolve the fleet-wide active subscription profile once, now: a later switch
538
+ // never changes this session (the operator restarts agents to move them).
539
+ const subscription = pinSubscriptionForLaunch(role, dir);
540
+ if (subscription?.pin.home)
541
+ deps.log(`[${name}] subscription: ${subscription.pin.provider} profile ${subscription.pin.profileId}`);
509
542
  // Preserve the role's existing isolation policy.
510
543
  let wrappedArgv = launch.argv;
511
544
  if (role.isolation) {
@@ -516,6 +549,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
516
549
  launch = { ...launch, argv: runtime.argv };
517
550
  const ctx = {
518
551
  ...isolationContextFor(role), stateDir: dir, runCwd,
552
+ subscriptionHome: subscription?.writablePath,
519
553
  runtimeReadPaths: [...runtime.readPaths, ...resolveLaunchRuntime([process.execPath, fileURLToPath(new URL('./agent-ours/bridge.js', import.meta.url))]).readPaths],
520
554
  };
521
555
  const policy = resolveIsolation(role.isolation, ctx);
@@ -539,8 +573,9 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
539
573
  wrappedArgv = [...rprefix, ...wrappedArgv];
540
574
  }
541
575
  const managedService = await deps.prepareAgentOurs(role, dir, opts.identityLifetime ? opts.identityLifetime === 'temporary' : temp);
576
+ let layoutRetiring = false;
542
577
  try {
543
- const managedHarness = prepareManagedHarness(role, dir, runCwd, managedService.descriptor, harnessChildEnv(role, launch.env, dir));
578
+ const managedHarness = prepareManagedHarness(role, dir, runCwd, managedService.descriptor, { ...harnessChildEnv(role, launch.env, dir), ...subscription?.env });
544
579
  // Start-stagger: space this launch at least start_stagger_ms after the previous
545
580
  // agent launch across the whole host, so a burst of boots (systemd starts every
546
581
  // user unit concurrently on boot; `ours-fleet up`/restart-all bulk-start) does not
@@ -599,6 +634,16 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
599
634
  let unsubscribeRecovery;
600
635
  let monitorLoop;
601
636
  let sessionStartupComplete = false;
637
+ let startupRecoveryAllowed = false;
638
+ let successfulTurnObserved = false;
639
+ let monitorMayStart = false;
640
+ let monitorStarted = false;
641
+ const startMonitorAfterStartup = () => {
642
+ if (!monitorStarted && monitorMayStart && sessionStartupComplete && !deps.shouldStop?.()) {
643
+ monitorStarted = true;
644
+ monitorLoop = monitor?.run(pid);
645
+ }
646
+ };
602
647
  let sessionClosed = false;
603
648
  let ownerChannel;
604
649
  let ownerBinder;
@@ -627,13 +672,23 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
627
672
  pid = agentSession.pid;
628
673
  arbiter = new RoleTurnArbiter(agentSession);
629
674
  sessionHandle = arbiter;
630
- unsubscribeRecovery = agentSession.subscribe(event => {
675
+ const unsubscribeFailure = agentSession.subscribe(event => {
631
676
  if (event.kind !== 'error' || !event.text || event.origin?.kind === 'stall-watchdog')
632
677
  return;
633
678
  const evidence = classifyFailureText(event.text, sessionBackend, new Date(deps.now()).toISOString());
634
679
  if (evidence)
635
680
  resolvedMonitorDeps.onFailureEvidence?.(evidence);
636
681
  });
682
+ const unsubscribeStartup = agentSession.subscribeConversation?.(event => {
683
+ if (event.kind !== 'turn.completed' || !('outcome' in event.payload) || event.payload.outcome !== 'completed')
684
+ return;
685
+ successfulTurnObserved = true;
686
+ if (startupRecoveryAllowed) {
687
+ sessionStartupComplete = true;
688
+ startMonitorAfterStartup();
689
+ }
690
+ });
691
+ unsubscribeRecovery = () => { unsubscribeFailure(); unsubscribeStartup?.(); };
637
692
  if (role.owner_channel) {
638
693
  try {
639
694
  ownerBinder = await deps.acquireOwnerBinder(dir, name, role.owner_channel.identity);
@@ -662,6 +717,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
662
717
  harness: role.harness,
663
718
  config: role.owner_channel,
664
719
  session: arbiter,
720
+ startupPending: () => !sessionStartupComplete,
665
721
  stateDir: dir,
666
722
  env: role.env,
667
723
  log: deps.log,
@@ -669,11 +725,18 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
669
725
  ...(configPath ? { configPath } : {}),
670
726
  });
671
727
  control = deps.createControlServer(dir, arbiter, deps.log);
728
+ control.setCorrespondence?.({ contacts: () => managedService.runtime.readContacts(), history: query => managedService.runtime.readHistory(query) });
672
729
  control.setFleetAuditor(ownerChannel ? {
673
730
  begin: (requestId, argv) => ownerChannel.beginFleetCommandAudit(requestId, argv),
674
731
  finish: input => ownerChannel.finishFleetCommandAudit(input),
675
732
  present: presentations => ownerChannel.notifyFleetLifecycle(presentations),
676
- } : localFleetAuditor(dir, name, deps.log));
733
+ } : localFleetAuditor(dir, name, deps.log, agentSession));
734
+ control.setLayoutControl?.(createLayoutControl({
735
+ agent: name, temporary: temp, standalone: !role.roomMemberStartup,
736
+ runtime: managedService.runtime, session: arbiter,
737
+ stopping: () => Boolean(deps.shouldStop?.()),
738
+ retire: async () => { layoutRetiring = true; await arbiter.close(); },
739
+ }));
677
740
  await control.start();
678
741
  }
679
742
  catch (error) {
@@ -704,10 +767,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
704
767
  // refusal or a cancellation reached the agent and was not acted on, so
705
768
  // the monitor must keep its cursor and try again.
706
769
  submit: async (text, options) => {
707
- // Cancelling the runner-owned startup prompt makes startup look failed
708
- // and closes the session before the wake turn can run. During startup,
709
- // steer into the live turn instead; after it completes, honor the
710
- // configured interrupt policy normally.
770
+ // Delivery starts after the protected startup turn succeeds. Honor the
771
+ // configured interrupt policy for subsequent turns.
711
772
  const policy = options?.interrupt;
712
773
  const interrupt = policy === true && sessionStartupComplete;
713
774
  const promptOptions = {
@@ -715,8 +776,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
715
776
  ...(interrupt ? { interruptSource: 'fleet-monitor' } : {}),
716
777
  origin: { kind: 'fleet-monitor' },
717
778
  };
718
- // Startup is already a protected boundary: as with immediate mode,
719
- // steer rather than waiting on/cancelling the runner-owned first turn.
779
+ // after_tool waits for a safe boundary in subsequent active turns.
720
780
  const result = policy === 'after_tool' && sessionStartupComplete
721
781
  ? await arbiter.submitPromptAfterTool(text, promptOptions)
722
782
  : await arbiter.submitPrompt(text, promptOptions);
@@ -752,10 +812,28 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
752
812
  // startup prompt and then refuses it has not started; logging the role as
753
813
  // up would hide a role that never read its briefing.
754
814
  const starting = arbiter.submitPrompt(firstPrompt, { origin: { kind: 'startup' } });
755
- // Monitoring starts immediately. The delivery adapter above downgrades
756
- // interruption to steering until this startup turn reaches a terminal
757
- // success, so there is neither a deaf gap nor a boot-cancellation loop.
758
- monitorLoop = monitor?.run(pid);
815
+ // Keep the early cursor baseline; do not deliver wakes into the protected
816
+ // first turn. Steering can pre-empt startup just like cancellation.
817
+ // Owner traffic and agent replies must be observed during a long first
818
+ // turn. Ordinary owner input steers until startup has proved successful.
819
+ if (ownerChannel) {
820
+ try {
821
+ await ownerChannel.start();
822
+ control.setOwnerChannel(ownerChannel);
823
+ }
824
+ catch (error) {
825
+ monitor?.stop();
826
+ if (monitorLoop)
827
+ await monitorLoop;
828
+ await control.close();
829
+ await ownerChannel.close().catch(() => undefined);
830
+ ownerBinder?.release();
831
+ await agentSession.close();
832
+ unsubscribeRecovery?.();
833
+ throw new Error(`[${name}] owner channel failed to start: `
834
+ + `${error?.message ?? String(error)}`);
835
+ }
836
+ }
759
837
  const started = await starting;
760
838
  // A temporary role's first turn can be the active turn when an ours wake
761
839
  // needs immediate attention. A typed console/monitor cancellation ends
@@ -768,6 +846,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
768
846
  if (!started.succeeded && !interruptedForWake) {
769
847
  monitor?.stop();
770
848
  await control.close();
849
+ await ownerChannel?.close().catch(() => undefined);
771
850
  ownerBinder?.release();
772
851
  await agentSession.close();
773
852
  unsubscribeRecovery?.();
@@ -794,27 +873,8 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
794
873
  else if (interruptedForWake)
795
874
  deps.log(`[${name}] ${sessionLabel} startup prompt cancelled by ${started.cancellationSource}; `
796
875
  + 'keeping temporary supervisor alive');
797
- sessionStartupComplete = true;
798
- if (ownerChannel) {
799
- try {
800
- await ownerChannel.start();
801
- }
802
- catch (error) {
803
- monitor?.stop();
804
- if (monitorLoop)
805
- await monitorLoop;
806
- await ownerChannel.close().catch(() => undefined);
807
- await control.close();
808
- ownerBinder?.release();
809
- await agentSession.close();
810
- unsubscribeRecovery?.();
811
- throw new Error(`[${name}] owner channel failed to start: `
812
- + `${error?.message ?? String(error)}`);
813
- }
814
- }
815
- if (ownerChannel) {
816
- control.setOwnerChannel(ownerChannel);
817
- }
876
+ startupRecoveryAllowed = interruptedForWake;
877
+ sessionStartupComplete = started.succeeded || (interruptedForWake && successfulTurnObserved);
818
878
  reloadLoopConfig = async () => {
819
879
  const nextRole = findRole(loadConfig(configPath), name);
820
880
  const definitions = nextRole.loops ?? [];
@@ -871,9 +931,10 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
871
931
  }
872
932
  }
873
933
  deps.log(`[${name}] up; pid=${pid} cwd=${runCwd} harness=${role.harness} session=${sessionBackend} mode=${mode}`);
874
- // The monitor loop lives exactly as long as the session: it starts once the
875
- // pane pid is known and is stopped when that pid dies (task dies with runner).
876
- monitorLoop ??= monitor?.run(pid);
934
+ // Keep arrivals behind the early prime() cursor; never re-prime at startup.
935
+ // Recoverable cancellation preserves the supervisor, not monitor readiness.
936
+ monitorMayStart = true;
937
+ startMonitorAfterStartup();
877
938
  recoveryController = new RoleRecoveryController({
878
939
  role: name, identity: role.identity, stateDir: dir, now: deps.now, sleep: deps.sleep,
879
940
  log: deps.log,
@@ -900,6 +961,14 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
900
961
  }
901
962
  },
902
963
  });
964
+ let chatIdle;
965
+ try {
966
+ if (temp && !role.roomMemberStartup)
967
+ chatIdle = new TemporaryChatIdle(dir, deps.now());
968
+ }
969
+ catch {
970
+ deps.log(`[${name}] automatic idle closure disabled: activity state is unreadable`);
971
+ }
903
972
  const start = deps.now();
904
973
  let nextLoopReloadAt = deps.now() + 30_000;
905
974
  let nextIdentityPollAt = deps.now();
@@ -923,6 +992,19 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
923
992
  void recoveryController.recover(observation.generation).catch(error => deps.log(`[${name}] daemon recovery controller failed: ${error?.name ?? 'Error'}`));
924
993
  }
925
994
  }
995
+ let idleExpired = false;
996
+ try {
997
+ idleExpired = chatIdle?.observe(sessionHandle.snapshot(), sessionHandle.conversationSnapshot?.().queueDepth ?? -1, now) ?? false;
998
+ }
999
+ catch {
1000
+ chatIdle = undefined;
1001
+ deps.log(`[${name}] automatic idle closure disabled: activity state could not be saved`);
1002
+ }
1003
+ if (idleExpired && await arbiter.retireIfIdle((snapshot, queue) => chatIdle.observe(snapshot, queue, deps.now()))) {
1004
+ retirementReason = 'idle-timeout';
1005
+ sessionClosed = true;
1006
+ break;
1007
+ }
926
1008
  if (deps.shouldStop?.()) {
927
1009
  if (temp) {
928
1010
  retirementReason = requestedTempStopReason(dir) ?? 'supervisor-signal';
@@ -934,7 +1016,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
934
1016
  }
935
1017
  if (temp && now >= nextIdentityPollAt) {
936
1018
  nextIdentityPollAt = now + TEMP_IDENTITY_POLL_MS;
937
- const presence = await probeIdentityPresence(role.identity, deps.fetch, resolvedMonitorDeps.env);
1019
+ const presence = await probeIdentityPresence(role.identity, deps.fetch, resolvedMonitorDeps.env, deps.identityProbeDeps);
938
1020
  if (presence.state === 'present') {
939
1021
  identityObserved = true;
940
1022
  identityAbsentSince = undefined;
@@ -971,6 +1053,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
971
1053
  }
972
1054
  }
973
1055
  }
1056
+ monitorMayStart = false;
974
1057
  if (loopManager) {
975
1058
  control?.setLoopManager(undefined);
976
1059
  await loopManager.stop();
@@ -1051,11 +1134,11 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
1051
1134
  throw new SupervisorRecycleRequiredError();
1052
1135
  return {
1053
1136
  elapsedSecs: elapsed, exit: exitRecord, rotated, mode, modelRecovery,
1054
- ...(retirementReason ? { retirementReason } : {}),
1137
+ ...(layoutRetiring ? { retirementReason: 'operator-stop' } : retirementReason ? { retirementReason } : {}),
1055
1138
  };
1056
1139
  }
1057
1140
  finally {
1058
- await managedService.close(false);
1141
+ await managedService.close(layoutRetiring);
1059
1142
  }
1060
1143
  }
1061
1144
  /**
@@ -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';
@@ -271,6 +272,8 @@ export declare class AcpSession implements AgentSession {
271
272
  * (`queuePrompt({ interrupt: true })`) needs the typed error, because only it
272
273
  * still owes an undelivered message a replay.
273
274
  */
275
+ readonly liveVoiceSupported = true;
276
+ interruptPrompt(generation: string, promptId: string): Promise<InterruptOutcome>;
274
277
  interrupt(source?: TurnCancellationSource): Promise<InterruptOutcome>;
275
278
  private cancelActive;
276
279
  /**
@@ -338,8 +341,13 @@ export declare class AcpSession implements AgentSession {
338
341
  * signal. Never infer commentary from text, message order, or unknown meta.
339
342
  */
340
343
  private codexMessagePhase;
341
- /** Normalize every ACP update losslessly into the durable ledger. */
344
+ /**
345
+ * Normalize every ACP update losslessly into the durable ledger. The ledger is the conversation the
346
+ * owner reads, so the agent's commentary between its tools is kept as written, like its final answer.
347
+ */
342
348
  private recordConversationUpdate;
349
+ taskNoticeBinding(): TaskNoticeBinding | undefined;
350
+ recordTaskCreated(binding: TaskNoticeBinding, notice: TaskCreatedNotice): void;
343
351
  conversationPage(request?: {
344
352
  after?: string;
345
353
  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';
@@ -7,6 +8,7 @@ import * as acp from '@agentclientprotocol/sdk';
7
8
  import { normalizeSessionUpdate } from './conversation-normalizer.js';
8
9
  import { ConversationEventStore } from './conversation-store.js';
9
10
  import { SessionEvents } from './events.js';
11
+ import { recordClaudeRateLimit } from '../subscriptions/observed.js';
10
12
  import { DEFAULT_STALL_TIMEOUT_MS, STALL_RECOVERY_PROMPT, StallWatchdog, StallToolHistory, hasStallRecoveryClaim } from './stall-watchdog.js';
11
13
  import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, classifyChildExit, sessionBackendCapabilities, turnResult, } from './types.js';
12
14
  const CANCEL_SETTLE_GRACE_MS = 15_000;
@@ -36,7 +38,6 @@ const TERMINAL_TOOL_STATUSES = new Set(['completed', 'failed']);
36
38
  /** Consumed only by Fleet's authenticated bundled-Codex app-server proxy. */
37
39
  export const CODEX_DISABLE_INHERITED_MCP_ENV = 'OURS_FLEET_CODEX_DISABLE_INHERITED_MCP';
38
40
  const SCHEDULED_LOOP_REDACTION = '[scheduled-loop content redacted]';
39
- const OWNER_COMMENTARY_REDACTION = '[assistant commentary redacted]';
40
41
  const MAX_CANONICAL_SYMLINK_DEPTH = 40;
41
42
  const scheduledTurn = (turn) => turn?.origin?.kind === 'scheduled-loop';
42
43
  /**
@@ -760,6 +761,10 @@ 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.');
766
+ if (command.requireIdle && (this.queueDepth > 0 || this.conversationSnapshot().readiness !== 'idle'))
767
+ throw new Error('voice_busy: Agent is already working. Wait for the current turn.');
763
768
  const queued = await this.queuePrompt(command.text, {
764
769
  origin: { kind: 'owner-admin-console', commandId: command.commandId },
765
770
  actor: { browserSession: command.actorBrowserSession },
@@ -792,6 +797,14 @@ export class AcpSession {
792
797
  * (`queuePrompt({ interrupt: true })`) needs the typed error, because only it
793
798
  * still owes an undelivered message a replay.
794
799
  */
800
+ liveVoiceSupported = true;
801
+ async interruptPrompt(generation, promptId) {
802
+ if (generation !== this.sessionGeneration)
803
+ throw new Error('session_changed: Agent session changed.');
804
+ if (this.activeTurn?.id !== promptId)
805
+ return { state: 'settled' };
806
+ return this.interrupt('local-console');
807
+ }
795
808
  async interrupt(source = 'local-console') {
796
809
  try {
797
810
  await this.cancelActive(source);
@@ -1707,9 +1720,15 @@ export class AcpSession {
1707
1720
  const scheduled = this.activeTurn?.origin?.kind === 'scheduled-loop';
1708
1721
  const messagePhase = update.sessionUpdate === 'agent_message_chunk'
1709
1722
  ? this.codexMessagePhase(update) : undefined;
1710
- this.recordConversationUpdate(update, scheduled, messagePhase === 'commentary');
1723
+ this.recordConversationUpdate(update, scheduled);
1711
1724
  if (update.sessionUpdate === 'config_option_update')
1712
1725
  this.captureRuntimeMetadata(update.configOptions);
1726
+ if (update.sessionUpdate === 'usage_update') {
1727
+ // Claude subscription rate-limit snapshot (claude-agent-acp forwards the SDK rate_limit_event).
1728
+ const rateLimit = update._meta?.['_claude/rateLimit'];
1729
+ if (rateLimit)
1730
+ recordClaudeRateLimit(this.options.stateDir, rateLimit);
1731
+ }
1713
1732
  switch (update.sessionUpdate) {
1714
1733
  case 'agent_message_chunk': {
1715
1734
  const phase = messagePhase;
@@ -1785,13 +1804,15 @@ export class AcpSession {
1785
1804
  return undefined;
1786
1805
  return phase === 'commentary' || phase === 'final_answer' ? phase : 'ambiguous';
1787
1806
  }
1788
- /** Normalize every ACP update losslessly into the durable ledger. */
1789
- recordConversationUpdate(update, scheduled, commentary = false) {
1807
+ /**
1808
+ * Normalize every ACP update losslessly into the durable ledger. The ledger is the conversation the
1809
+ * owner reads, so the agent's commentary between its tools is kept as written, like its final answer.
1810
+ */
1811
+ recordConversationUpdate(update, scheduled) {
1790
1812
  const normalized = normalizeSessionUpdate(update, scheduled ? {
1791
1813
  redactText: SCHEDULED_LOOP_REDACTION,
1792
1814
  redactToolCallId: 'scheduled-loop-tool',
1793
- }
1794
- : commentary ? { redactText: OWNER_COMMENTARY_REDACTION } : {});
1815
+ } : {});
1795
1816
  this.conversation.appendSafe({
1796
1817
  kind: normalized.kind,
1797
1818
  sessionGeneration: this.sessionGeneration,
@@ -1805,6 +1826,10 @@ export class AcpSession {
1805
1826
  });
1806
1827
  }
1807
1828
  // ── conversation ledger access (AgentSession) ─────────────────────────────
1829
+ taskNoticeBinding() {
1830
+ return this.activeTurn && this.sessionId ? { sessionGeneration: this.sessionGeneration, acpSessionId: this.sessionId, promptId: this.activeTurn.id } : undefined;
1831
+ }
1832
+ recordTaskCreated(binding, notice) { appendTaskCreated(this.conversation, binding, notice); }
1808
1833
  conversationPage(request = {}) {
1809
1834
  const floor = Number(this.conversationStartCursor ?? 0);
1810
1835
  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;
@@ -72,6 +75,8 @@ export declare class RoleTurnArbiter implements AgentSession {
72
75
  conversationSnapshot(): ConversationSnapshot;
73
76
  subscribeConversation(listener: (event: ConversationEventV1) => void): () => void;
74
77
  submitPromptBrowser(command: SubmitPromptCommand): Promise<PromptReceipt>;
78
+ get liveVoiceSupported(): boolean;
79
+ interruptPrompt(generation: string, promptId: string): Promise<InterruptOutcome>;
75
80
  interrupt(source?: TurnCancellationSource): Promise<InterruptOutcome>;
76
81
  respondPermission(permissionId: string, optionId: string): boolean;
77
82
  respondPermissionV2(permissionId: string, optionId: string, sessionGeneration: string): 'accepted' | 'stale';
@@ -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,16 @@ 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); });
179
+ }
180
+ get liveVoiceSupported() { return this.session.liveVoiceSupported === true; }
181
+ interruptPrompt(generation, promptId) {
182
+ return this.exclusive(async () => {
183
+ if (!this.session.interruptPrompt)
184
+ throw new Error('targeted voice interrupt is unavailable');
185
+ return interruptOutcome(await this.session.interruptPrompt(generation, promptId));
186
+ });
161
187
  }
162
188
  interrupt(source = 'local-console') {
163
189
  return this.exclusive(async () => interruptOutcome(await this.session.interrupt(source)));