@ours.network/fleet 1.2.0-nightly.8 → 1.2.0-nightly.81

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 (429) hide show
  1. package/README.md +280 -68
  2. package/dist/account-origin.d.ts +5 -0
  3. package/dist/account-origin.js +31 -0
  4. package/dist/agent-ours/bridge.js +26 -9
  5. package/dist/agent-ours/correspondence.d.ts +10 -0
  6. package/dist/agent-ours/correspondence.js +1 -0
  7. package/dist/agent-ours/file-rpc.d.ts +12 -0
  8. package/dist/agent-ours/file-rpc.js +18 -4
  9. package/dist/agent-ours/harness.js +9 -1
  10. package/dist/agent-ours/mcp-endpoint.d.ts +22 -0
  11. package/dist/agent-ours/mcp-endpoint.js +101 -25
  12. package/dist/agent-ours/runtime.d.ts +9 -1
  13. package/dist/agent-ours/runtime.js +50 -2
  14. package/dist/agent-ours/service.d.ts +27 -1
  15. package/dist/agent-ours/service.js +107 -13
  16. package/dist/application/codex-models.d.ts +15 -0
  17. package/dist/application/codex-models.js +56 -0
  18. package/dist/application/conversation-history.d.ts +34 -0
  19. package/dist/application/conversation-history.js +294 -0
  20. package/dist/application/conversation-role-observer.d.ts +62 -0
  21. package/dist/application/conversation-role-observer.js +206 -0
  22. package/dist/application/fleet-query-service.d.ts +1 -1
  23. package/dist/application/fleet-query-service.js +3 -3
  24. package/dist/application/log-service.js +8 -8
  25. package/dist/application/model-catalog.d.ts +1 -1
  26. package/dist/application/model-catalog.js +5 -2
  27. package/dist/application/preset-provenance.d.ts +19 -0
  28. package/dist/application/preset-provenance.js +59 -0
  29. package/dist/application/resource-observer.d.ts +43 -0
  30. package/dist/application/resource-observer.js +323 -0
  31. package/dist/application/role-creation-service.js +3 -2
  32. package/dist/application/role-removal-service.js +17 -3
  33. package/dist/application/role-repository.d.ts +1 -1
  34. package/dist/application/role-repository.js +30 -7
  35. package/dist/application/room-layout-definitions.d.ts +45 -0
  36. package/dist/application/room-layout-definitions.js +130 -0
  37. package/dist/application/session-control.d.ts +63 -17
  38. package/dist/application/session-control.js +221 -23
  39. package/dist/application/supervisor-ours-tools.js +5 -11
  40. package/dist/application/task-layouts.d.ts +91 -0
  41. package/dist/application/task-layouts.js +261 -0
  42. package/dist/application/task-room-service.d.ts +101 -3
  43. package/dist/application/task-room-service.js +286 -33
  44. package/dist/application/types.d.ts +2 -0
  45. package/dist/atomic-file.d.ts +34 -1
  46. package/dist/atomic-file.js +161 -18
  47. package/dist/briefing.d.ts +3 -1
  48. package/dist/briefing.js +120 -31
  49. package/dist/build-info.json +9 -4
  50. package/dist/capabilities.d.ts +9 -1
  51. package/dist/capabilities.js +13 -0
  52. package/dist/cli.js +402 -38
  53. package/dist/client-profile.d.ts +5 -17
  54. package/dist/client-profile.js +4 -127
  55. package/dist/config.d.ts +45 -0
  56. package/dist/config.js +57 -6
  57. package/dist/creation.d.ts +1 -1
  58. package/dist/creation.js +4 -4
  59. package/dist/daemon-recovery.d.ts +14 -9
  60. package/dist/daemon-recovery.js +124 -115
  61. package/dist/docs.d.ts +1 -1
  62. package/dist/docs.js +227 -19
  63. package/dist/doctor.d.ts +4 -1
  64. package/dist/doctor.js +75 -60
  65. package/dist/erased-resources.d.ts +9 -0
  66. package/dist/erased-resources.js +87 -0
  67. package/dist/file-delivery/reader.d.ts +15 -0
  68. package/dist/file-delivery/reader.js +63 -0
  69. package/dist/file-delivery/store.d.ts +15 -0
  70. package/dist/file-delivery/store.js +124 -0
  71. package/dist/file-delivery/types.d.ts +19 -0
  72. package/dist/file-delivery/types.js +1 -0
  73. package/dist/fleet-command-audit.d.ts +13 -0
  74. package/dist/fleet-command-audit.js +31 -9
  75. package/dist/harness/agent-session.d.ts +4 -0
  76. package/dist/harness/claude-code-session.js +1 -1
  77. package/dist/harness/claude-code.js +21 -11
  78. package/dist/harness/claude-runtime.d.ts +12 -0
  79. package/dist/harness/claude-runtime.js +43 -0
  80. package/dist/harness/codex-app-server-proxy.d.ts +4 -1
  81. package/dist/harness/codex-app-server-proxy.js +41 -5
  82. package/dist/harness/codex-reasoning.d.ts +5 -0
  83. package/dist/harness/codex-reasoning.js +12 -0
  84. package/dist/harness/codex-runtime.d.ts +11 -1
  85. package/dist/harness/codex-runtime.js +31 -4
  86. package/dist/harness/codex-session.js +2 -2
  87. package/dist/harness/codex.js +24 -12
  88. package/dist/init-wizard.d.ts +2 -0
  89. package/dist/init-wizard.js +38 -19
  90. package/dist/isolation/policy.js +6 -1
  91. package/dist/isolation/types.d.ts +5 -0
  92. package/dist/managed-cli.d.ts +253 -0
  93. package/dist/managed-cli.js +923 -0
  94. package/dist/managed-recovery.d.ts +13 -0
  95. package/dist/managed-recovery.js +31 -0
  96. package/dist/minimal-setup.d.ts +2 -0
  97. package/dist/minimal-setup.js +44 -0
  98. package/dist/monitor.d.ts +9 -34
  99. package/dist/monitor.js +90 -153
  100. package/dist/notifications/fleet-producer.d.ts +31 -0
  101. package/dist/notifications/fleet-producer.js +351 -0
  102. package/dist/notifications/outbox.d.ts +59 -0
  103. package/dist/notifications/outbox.js +199 -0
  104. package/dist/notifications/target-cleanup.d.ts +16 -0
  105. package/dist/notifications/target-cleanup.js +75 -0
  106. package/dist/notifications/task-producer.d.ts +20 -0
  107. package/dist/notifications/task-producer.js +100 -0
  108. package/dist/ops.d.ts +2 -0
  109. package/dist/ops.js +20 -3
  110. package/dist/owner-channel/channel.js +17 -6
  111. package/dist/owner-channel/commands.d.ts +4 -0
  112. package/dist/owner-channel/commands.js +8 -0
  113. package/dist/owner-channel/lifecycle-outbox.js +4 -3
  114. package/dist/owner-channel/ours-client.js +2 -17
  115. package/dist/paths.d.ts +5 -0
  116. package/dist/paths.js +5 -0
  117. package/dist/preset-migration.js +55 -0
  118. package/dist/process-incarnation.d.ts +23 -0
  119. package/dist/process-incarnation.js +42 -0
  120. package/dist/reminders/manager.d.ts +75 -0
  121. package/dist/reminders/manager.js +278 -0
  122. package/dist/reminders/runtime.d.ts +27 -0
  123. package/dist/reminders/runtime.js +32 -0
  124. package/dist/reminders/schedule.d.ts +37 -0
  125. package/dist/reminders/schedule.js +104 -0
  126. package/dist/reminders/store.d.ts +341 -0
  127. package/dist/reminders/store.js +762 -0
  128. package/dist/reminders/tools.d.ts +20 -0
  129. package/dist/reminders/tools.js +114 -0
  130. package/dist/resolved-plan.js +1 -0
  131. package/dist/rooms-tasks/add-member.d.ts +48 -0
  132. package/dist/rooms-tasks/add-member.js +84 -0
  133. package/dist/rooms-tasks/archived-absence.d.ts +4 -0
  134. package/dist/rooms-tasks/archived-absence.js +11 -0
  135. package/dist/rooms-tasks/cli.js +182 -5
  136. package/dist/rooms-tasks/close.d.ts +5 -2
  137. package/dist/rooms-tasks/close.js +224 -36
  138. package/dist/rooms-tasks/config.d.ts +1 -1
  139. package/dist/rooms-tasks/config.js +11 -4
  140. package/dist/rooms-tasks/cowork-adapter.d.ts +12 -5
  141. package/dist/rooms-tasks/cowork-adapter.js +15 -121
  142. package/dist/rooms-tasks/cowork-http.js +56 -6
  143. package/dist/rooms-tasks/deletion.d.ts +1 -1
  144. package/dist/rooms-tasks/deletion.js +66 -10
  145. package/dist/rooms-tasks/erasure.d.ts +6 -0
  146. package/dist/rooms-tasks/erasure.js +173 -0
  147. package/dist/rooms-tasks/external-worker.d.ts +1 -1
  148. package/dist/rooms-tasks/external-worker.js +3 -4
  149. package/dist/rooms-tasks/layout-binding-client.d.ts +11 -0
  150. package/dist/rooms-tasks/layout-binding-client.js +56 -0
  151. package/dist/rooms-tasks/layout-binding-grants.d.ts +21 -0
  152. package/dist/rooms-tasks/layout-binding-grants.js +128 -0
  153. package/dist/rooms-tasks/layout-cli.d.ts +4 -0
  154. package/dist/rooms-tasks/layout-cli.js +107 -0
  155. package/dist/rooms-tasks/layout-config.d.ts +11 -0
  156. package/dist/rooms-tasks/layout-config.js +89 -0
  157. package/dist/rooms-tasks/layout-control.d.ts +21 -0
  158. package/dist/rooms-tasks/layout-control.js +52 -0
  159. package/dist/rooms-tasks/layout-member-retirement.d.ts +10 -0
  160. package/dist/rooms-tasks/layout-member-retirement.js +90 -0
  161. package/dist/rooms-tasks/layout-service.d.ts +43 -0
  162. package/dist/rooms-tasks/layout-service.js +203 -0
  163. package/dist/rooms-tasks/layout.d.ts +121 -0
  164. package/dist/rooms-tasks/layout.js +286 -0
  165. package/dist/rooms-tasks/legacy-task-member.d.ts +19 -0
  166. package/dist/rooms-tasks/legacy-task-member.js +116 -0
  167. package/dist/rooms-tasks/live-readiness.d.ts +18 -0
  168. package/dist/rooms-tasks/live-readiness.js +85 -0
  169. package/dist/rooms-tasks/member-ownership.d.ts +5 -0
  170. package/dist/rooms-tasks/member-ownership.js +40 -0
  171. package/dist/rooms-tasks/member-startup.d.ts +1 -0
  172. package/dist/rooms-tasks/member-startup.js +2 -0
  173. package/dist/rooms-tasks/provision.d.ts +3 -0
  174. package/dist/rooms-tasks/provision.js +101 -49
  175. package/dist/rooms-tasks/recovery.d.ts +19 -0
  176. package/dist/rooms-tasks/recovery.js +110 -0
  177. package/dist/rooms-tasks/room-state.d.ts +4 -2
  178. package/dist/rooms-tasks/room-state.js +47 -8
  179. package/dist/rooms-tasks/task-review.d.ts +113 -0
  180. package/dist/rooms-tasks/task-review.js +561 -0
  181. package/dist/rooms-tasks/task-service-retirement.d.ts +11 -0
  182. package/dist/rooms-tasks/task-service-retirement.js +21 -0
  183. package/dist/rooms-tasks/task-state.d.ts +51 -5
  184. package/dist/rooms-tasks/task-state.js +358 -50
  185. package/dist/rooms-tasks/task-tools.d.ts +35 -0
  186. package/dist/rooms-tasks/task-tools.js +134 -0
  187. package/dist/rooms-tasks/types.d.ts +78 -0
  188. package/dist/rooms-tasks/workspace-artifacts.d.ts +6 -0
  189. package/dist/rooms-tasks/workspace-artifacts.js +93 -0
  190. package/dist/rooms-tasks/workspace.d.ts +35 -0
  191. package/dist/rooms-tasks/workspace.js +325 -0
  192. package/dist/runner.d.ts +33 -4
  193. package/dist/runner.js +410 -62
  194. package/dist/session/acp.d.ts +75 -2
  195. package/dist/session/acp.js +345 -82
  196. package/dist/session/arbiter.d.ts +98 -0
  197. package/dist/session/arbiter.js +272 -3
  198. package/dist/session/codex-app-server.d.ts +45 -1
  199. package/dist/session/codex-app-server.js +225 -32
  200. package/dist/session/control.d.ts +17 -3
  201. package/dist/session/control.js +219 -23
  202. package/dist/session/conversation-normalizer.d.ts +2 -1
  203. package/dist/session/conversation-normalizer.js +1 -1
  204. package/dist/session/conversation-observation.d.ts +25 -0
  205. package/dist/session/conversation-observation.js +100 -0
  206. package/dist/session/conversation-replay.d.ts +50 -0
  207. package/dist/session/conversation-replay.js +203 -0
  208. package/dist/session/conversation-store.d.ts +15 -1
  209. package/dist/session/conversation-store.js +213 -26
  210. package/dist/session/conversation-types.d.ts +57 -4
  211. package/dist/session/prompt-batch.d.ts +79 -0
  212. package/dist/session/prompt-batch.js +120 -0
  213. package/dist/session/task-notice.d.ts +14 -0
  214. package/dist/session/task-notice.js +13 -0
  215. package/dist/session/types.d.ts +42 -1
  216. package/dist/setup-tunnel-preflight.d.ts +34 -0
  217. package/dist/setup-tunnel-preflight.js +62 -0
  218. package/dist/socket-path.d.ts +2 -0
  219. package/dist/socket-path.js +22 -0
  220. package/dist/spawn.d.ts +5 -1
  221. package/dist/spawn.js +40 -4
  222. package/dist/startup-prompt.d.ts +15 -0
  223. package/dist/startup-prompt.js +71 -0
  224. package/dist/startup-readiness.d.ts +32 -0
  225. package/dist/startup-readiness.js +101 -0
  226. package/dist/startup-telemetry.d.ts +16 -0
  227. package/dist/startup-telemetry.js +46 -0
  228. package/dist/subscriptions/cli.d.ts +91 -0
  229. package/dist/subscriptions/cli.js +223 -0
  230. package/dist/subscriptions/launch.d.ts +37 -0
  231. package/dist/subscriptions/launch.js +93 -0
  232. package/dist/subscriptions/login.d.ts +77 -0
  233. package/dist/subscriptions/login.js +215 -0
  234. package/dist/subscriptions/observed.d.ts +7 -0
  235. package/dist/subscriptions/observed.js +44 -0
  236. package/dist/subscriptions/probe.d.ts +10 -0
  237. package/dist/subscriptions/probe.js +73 -0
  238. package/dist/subscriptions/service.d.ts +102 -0
  239. package/dist/subscriptions/service.js +364 -0
  240. package/dist/subscriptions/store.d.ts +71 -0
  241. package/dist/subscriptions/store.js +143 -0
  242. package/dist/supervisor/adoption.d.ts +8 -0
  243. package/dist/supervisor/adoption.js +78 -0
  244. package/dist/supervisor/catalog.d.ts +62 -0
  245. package/dist/supervisor/catalog.js +254 -0
  246. package/dist/supervisor/fleet.d.ts +8 -0
  247. package/dist/supervisor/fleet.js +89 -0
  248. package/dist/supervisor/index.js +3 -6
  249. package/dist/supervisor/launchd.d.ts +1 -1
  250. package/dist/supervisor/launchd.js +26 -13
  251. package/dist/supervisor/legacy.d.ts +35 -0
  252. package/dist/supervisor/legacy.js +524 -0
  253. package/dist/supervisor/manager.d.ts +15 -0
  254. package/dist/supervisor/manager.js +270 -0
  255. package/dist/supervisor/registration-refresh.d.ts +45 -0
  256. package/dist/supervisor/registration-refresh.js +266 -0
  257. package/dist/supervisor/scope.d.ts +4 -0
  258. package/dist/supervisor/scope.js +10 -0
  259. package/dist/supervisor/systemd.d.ts +2 -2
  260. package/dist/supervisor/systemd.js +19 -33
  261. package/dist/task-supervision.d.ts +9 -0
  262. package/dist/task-supervision.js +145 -0
  263. package/dist/temp-idle.d.ts +17 -0
  264. package/dist/temp-idle.js +42 -0
  265. package/dist/temp-lifecycle.d.ts +23 -8
  266. package/dist/temp-lifecycle.js +123 -66
  267. package/dist/temp-supervisor-recovery.d.ts +1 -1
  268. package/dist/temp-supervisor-recovery.js +2 -2
  269. package/dist/tunnel-setup-completion.d.ts +7 -0
  270. package/dist/tunnel-setup-completion.js +13 -0
  271. package/dist/tunnel-setup-input.d.ts +19 -0
  272. package/dist/tunnel-setup-input.js +130 -0
  273. package/dist/watchdog/query.d.ts +3 -1
  274. package/dist/watchdog/query.js +2 -1
  275. package/dist/web/agent-attachments.d.ts +29 -0
  276. package/dist/web/agent-attachments.js +162 -0
  277. package/dist/web/auth.d.ts +65 -2
  278. package/dist/web/auth.js +201 -11
  279. package/dist/web/available-port.d.ts +2 -0
  280. package/dist/web/available-port.js +23 -0
  281. package/dist/web/control.d.ts +3 -3
  282. package/dist/web/control.js +10 -8
  283. package/dist/web/conversation-stream-codec.d.ts +23 -0
  284. package/dist/web/conversation-stream-codec.js +110 -0
  285. package/dist/web/conversation-stream.d.ts +26 -0
  286. package/dist/web/conversation-stream.js +239 -0
  287. package/dist/web/events.d.ts +2 -0
  288. package/dist/web/events.js +9 -0
  289. package/dist/web/fleet-config-service.d.ts +7 -1
  290. package/dist/web/fleet-config-service.js +61 -16
  291. package/dist/web/prefix-gateway.d.ts +32 -0
  292. package/dist/web/prefix-gateway.js +248 -0
  293. package/dist/web/resource-views.d.ts +135 -0
  294. package/dist/web/resource-views.js +494 -0
  295. package/dist/web/runtime.d.ts +3 -1
  296. package/dist/web/runtime.js +132 -34
  297. package/dist/web/server.d.ts +19 -0
  298. package/dist/web/server.js +893 -71
  299. package/dist/web/service.d.ts +11 -2
  300. package/dist/web/service.js +30 -4
  301. package/dist/web/workspace-devices.d.ts +107 -0
  302. package/dist/web/workspace-devices.js +277 -0
  303. package/dist/web/workspace-tunnel.d.ts +1 -0
  304. package/dist/web/workspace-tunnel.js +34 -0
  305. package/dist/workspace-enrollment.d.ts +83 -0
  306. package/dist/workspace-enrollment.js +605 -0
  307. package/dist/workspace-replacement.d.ts +66 -0
  308. package/dist/workspace-replacement.js +238 -0
  309. package/docs/acp-files.md +146 -0
  310. package/docs/conversation-stream.md +216 -0
  311. package/docs/fleet-migration.md +54 -0
  312. package/docs/live-voice.md +13 -0
  313. package/docs/room-layouts.md +227 -0
  314. package/docs/self-reminders.md +374 -0
  315. package/docs/task-progress-review.md +143 -0
  316. package/docs/task-workspaces.md +225 -0
  317. package/docs/validation/managed-cli-permissions.md +421 -0
  318. package/examples/room-layouts/fresh.yaml +27 -0
  319. package/examples/room-layouts/scoped.yaml +21 -0
  320. package/examples/room-layouts/shared.yaml +19 -0
  321. package/package.json +25 -20
  322. package/presets/README.md +78 -0
  323. package/presets/brain-catalog.json +8 -2
  324. package/presets/fleet/agent_templates/Critic.yaml +1 -1
  325. package/presets/fleet/agent_templates/Developer.yaml +1 -1
  326. package/presets/fleet/agent_templates/Engineer.yaml +5 -0
  327. package/presets/fleet/agent_templates/LocalCoordinator.yaml +6 -3
  328. package/presets/fleet/agents/FleetCoordinator.yaml +1 -1
  329. package/presets/fleet/brains/claude-fable-5-1-high.yaml +5 -0
  330. package/presets/fleet/brains/claude-fable-5-1-low.yaml +5 -0
  331. package/presets/fleet/brains/claude-fable-5-1-max.yaml +5 -0
  332. package/presets/fleet/brains/claude-fable-5-1-medium.yaml +5 -0
  333. package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +5 -0
  334. package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
  335. package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
  336. package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
  337. package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
  338. package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
  339. package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +4 -0
  340. package/presets/fleet/brains/claude-opus-5-5-high.yaml +5 -0
  341. package/presets/fleet/brains/claude-opus-5-5-low.yaml +5 -0
  342. package/presets/fleet/brains/claude-opus-5-5-max.yaml +5 -0
  343. package/presets/fleet/brains/claude-opus-5-5-medium.yaml +5 -0
  344. package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +5 -0
  345. package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
  346. package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
  347. package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
  348. package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
  349. package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
  350. package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
  351. package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
  352. package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
  353. package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
  354. package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
  355. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
  356. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
  357. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
  358. package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
  359. package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
  360. package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
  361. package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
  362. package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
  363. package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
  364. package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
  365. package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
  366. package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
  367. package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
  368. package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
  369. package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
  370. package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
  371. package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
  372. package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
  373. package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
  374. package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
  375. package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
  376. package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
  377. package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
  378. package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
  379. package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
  380. package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
  381. package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
  382. package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
  383. package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
  384. package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
  385. package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
  386. package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
  387. package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
  388. package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
  389. package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
  390. package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
  391. package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
  392. package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
  393. package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
  394. package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
  395. package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
  396. package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
  397. package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
  398. package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
  399. package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
  400. package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +5 -0
  401. package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +5 -0
  402. package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +5 -0
  403. package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +5 -0
  404. package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +5 -0
  405. package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +5 -0
  406. package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +5 -0
  407. package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +5 -0
  408. package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +5 -0
  409. package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +5 -0
  410. package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +5 -0
  411. package/presets/fleet/roles/Coordinator.yaml +21 -13
  412. package/presets/fleet/roles/Critic.yaml +24 -0
  413. package/presets/fleet/roles/Developer.yaml +32 -0
  414. package/presets/fleet/roles/Engineer.yaml +72 -0
  415. package/presets/fleet/roles/LocalCoordinator.yaml +17 -0
  416. package/presets/fleet/room_layouts/engineering.yaml +25 -0
  417. package/presets/fleet/room_layouts/pair.yaml +24 -0
  418. package/presets/fleet/room_layouts/single.yaml +17 -0
  419. package/presets/fleet/room_layouts/team.yaml +30 -0
  420. package/presets/fleet/room_templates/engineering.yaml +19 -0
  421. package/presets/manifest.json +2 -2
  422. package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
  423. package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
  424. package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
  425. package/dist/web-app/icons/ours-fleet.svg +0 -4
  426. package/dist/web-app/index.html +0 -17
  427. package/dist/web-app/manifest.webmanifest +0 -15
  428. package/dist/web-app/offline.html +0 -18
  429. package/dist/web-app/sw.js +0 -51
@@ -0,0 +1,762 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { chmodSync, existsSync, lstatSync, readFileSync, readdirSync, renameSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { replaceFileAtomically } from '../atomic-file.js';
5
+ import { ReminderScheduleError, resolveSchedule } from './schedule.js';
6
+ /**
7
+ * Durable, private, single-writer state for one agent's self-reminders.
8
+ *
9
+ * The runner that supervises the agent is the only writer: the agent's MCP
10
+ * tools, the dispatch timer and the session's release guard all run in that
11
+ * one process, and every mutation here is synchronous, so each method call is
12
+ * its own linearization point. Asynchronous callers (a queue receipt, a turn
13
+ * completion) never carry state across an `await`; they re-read the current
14
+ * record and apply only forward transitions, so a late receipt can never undo
15
+ * an acknowledgement, update or cancellation that happened meanwhile.
16
+ *
17
+ * Event lifecycle — the boundary that matters is `dispatched`:
18
+ *
19
+ * pending ──admit──▶ admitting ──release guard──▶ dispatched ──ack──▶ acknowledged
20
+ * ▲ │ │
21
+ * └── not released ───┘ └─ restart ─▶ uncertain (held)
22
+ *
23
+ * `dispatched` is written durably by the session's release guard immediately
24
+ * before the prompt is handed to the model. An event found `admitting` after a
25
+ * restart therefore provably never reached the model (reminder prompt bodies
26
+ * are not retained, so session recovery cannot replay them) and is retried
27
+ * under the same event id. An event found `dispatched` may or may not have been
28
+ * seen, so it is held as `uncertain` and never automatically re-admitted.
29
+ *
30
+ * Urgency is an explicit opt-in per reminder (`interrupt`). Absent means false,
31
+ * so stores written before it existed keep their exact idle-only meaning, and
32
+ * an older runtime that ignores the field stays idle-only. An urgent event may
33
+ * request at most ONE interruption of the agent's current turn per revision:
34
+ * the request is durably recorded before the session is asked to cancel, so a
35
+ * crash, a forced adapter restart or a second urgent reminder can never turn
36
+ * it into a cancellation loop. Every later admission of that event is
37
+ * idle-only, and only the release guard ever records `dispatched`.
38
+ */
39
+ export const SELF_REMINDERS_FILE = '.self-reminders.json';
40
+ export const MAX_OPEN_REMINDERS = 100;
41
+ export const MAX_RETAINED_CLOSED = 200;
42
+ export const DEFAULT_LATE_WINDOW_SECONDS = 3600;
43
+ export const MAX_LATE_WINDOW_SECONDS = 7 * 24 * 3600;
44
+ export const MIN_LATE_WINDOW_SECONDS = 60;
45
+ const MAX_HORIZON_MS = 5 * 366 * 24 * 3600 * 1000;
46
+ const MAX_TITLE_CHARS = 200;
47
+ const MAX_NOTE_CHARS = 4000;
48
+ const MAX_REQUEST_KEY_CHARS = 200;
49
+ const INTERRUPT_OUTCOMES = new Set(['requested', 'cancelled', 'ended', 'forced', 'failed', 'unknown']);
50
+ export class ReminderError extends Error {
51
+ code;
52
+ constructor(code, message) {
53
+ super(message);
54
+ this.code = code;
55
+ this.name = 'ReminderError';
56
+ }
57
+ }
58
+ const durableWrite = (path, contents) => replaceFileAtomically(path, contents, 0o600, {}, { durable: true });
59
+ const OPEN_EVENT_STATES = new Set(['pending', 'admitting', 'dispatched', 'uncertain']);
60
+ const iso = (ms) => new Date(ms).toISOString();
61
+ function digest(value) {
62
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
63
+ }
64
+ function text(raw, field, max, required) {
65
+ if (raw === undefined || raw === null) {
66
+ if (required)
67
+ throw new ReminderError('invalid_request', `${field} is required`);
68
+ return '';
69
+ }
70
+ if (typeof raw !== 'string')
71
+ throw new ReminderError('invalid_request', `${field} must be text`);
72
+ const value = raw.replace(/\r\n?/g, '\n').normalize('NFC');
73
+ if (required && !value.trim())
74
+ throw new ReminderError('invalid_request', `${field} must not be blank`);
75
+ if (value.includes('\0'))
76
+ throw new ReminderError('invalid_request', `${field} must not contain NUL`);
77
+ if (Array.from(value).length > max)
78
+ throw new ReminderError('invalid_request', `${field} exceeds ${max} characters`);
79
+ return value;
80
+ }
81
+ function lateWindow(raw) {
82
+ if (raw === undefined || raw === null)
83
+ return DEFAULT_LATE_WINDOW_SECONDS;
84
+ if (typeof raw !== 'number' || !Number.isSafeInteger(raw)
85
+ || raw < MIN_LATE_WINDOW_SECONDS || raw > MAX_LATE_WINDOW_SECONDS)
86
+ throw new ReminderError('invalid_request', `late_window_seconds must be a whole number from ${MIN_LATE_WINDOW_SECONDS} to ${MAX_LATE_WINDOW_SECONDS}`);
87
+ return raw;
88
+ }
89
+ function revisionOf(raw) {
90
+ if (typeof raw !== 'number' || !Number.isSafeInteger(raw) || raw < 1)
91
+ throw new ReminderError('invalid_request', 'expected_revision must be the current positive revision');
92
+ return raw;
93
+ }
94
+ function urgency(raw) {
95
+ if (raw === undefined || raw === null)
96
+ return false;
97
+ if (typeof raw !== 'boolean')
98
+ throw new ReminderError('invalid_request', 'interrupt must be true or false');
99
+ return raw;
100
+ }
101
+ export class ReminderStore {
102
+ owner;
103
+ now;
104
+ log;
105
+ write;
106
+ path;
107
+ state;
108
+ /**
109
+ * Load or create the store for exactly this owner (see `ReminderOwner`). A
110
+ * store written for anyone else is never adopted: it is moved aside and
111
+ * nothing in it is delivered, so a same-named successor or a replaced
112
+ * identity cannot inherit a predecessor's wakes.
113
+ */
114
+ constructor(stateDir, owner, now, log = () => { }, write = durableWrite) {
115
+ this.owner = owner;
116
+ this.now = now;
117
+ this.log = log;
118
+ this.write = write;
119
+ this.path = join(stateDir, SELF_REMINDERS_FILE);
120
+ let restored;
121
+ if (existsSync(this.path)) {
122
+ let reason;
123
+ try {
124
+ if (!privateFile(this.path))
125
+ throw new Error('insecure');
126
+ const parsed = JSON.parse(readFileSync(this.path, 'utf8'));
127
+ if (!validFile(parsed))
128
+ reason = 'corrupt';
129
+ else if (!sameOwner(parsed.owner, owner))
130
+ reason = 'foreign-owner';
131
+ else {
132
+ // A permanent agent's runtime is re-provisioned on every orderly
133
+ // start; record the current one, keep the reminders.
134
+ parsed.owner = { ...owner };
135
+ restored = parsed;
136
+ }
137
+ }
138
+ catch {
139
+ reason = 'corrupt';
140
+ }
141
+ if (!restored) {
142
+ const aside = `${this.path}.${reason}-${now()}`;
143
+ renameSync(this.path, aside);
144
+ chmodSync(aside, 0o600);
145
+ this.log(`[${owner.role}] self-reminder store ${reason}; moved aside, nothing in it will be delivered`);
146
+ }
147
+ }
148
+ this.state = restored ?? { version: 1, owner: { ...owner }, reminders: {}, events: {} };
149
+ if (restored)
150
+ this.recover();
151
+ this.persist();
152
+ }
153
+ /** Restart recovery. Never re-admits anything that may have reached the model. */
154
+ recover() {
155
+ const at = iso(this.now());
156
+ for (const event of Object.values(this.state.events)) {
157
+ // An interruption requested by the previous process has an unknown end.
158
+ // Keep the mark (so it is never requested again) and say so.
159
+ if (event.interrupt?.outcome === 'requested')
160
+ event.interrupt = { ...event.interrupt, outcome: 'unknown', settledAt: at, reason: 'restarted_before_outcome' };
161
+ if (event.state === 'admitting') {
162
+ // The release guard never ran, so the model never saw it.
163
+ event.state = 'pending';
164
+ delete event.promptId;
165
+ delete event.retryAt;
166
+ }
167
+ else if (event.state === 'dispatched') {
168
+ event.state = 'uncertain';
169
+ event.reason = 'restarted_after_dispatch';
170
+ event.closedAt = at;
171
+ }
172
+ }
173
+ }
174
+ persist() {
175
+ this.write(this.path, JSON.stringify(this.state, null, 2) + '\n');
176
+ chmodSync(this.path, 0o600);
177
+ }
178
+ /**
179
+ * Apply a mutation and persist it as one step. If the write fails the
180
+ * in-memory state is restored, so memory never claims what disk does not.
181
+ */
182
+ commit(mutate) {
183
+ const before = structuredClone(this.state);
184
+ try {
185
+ const result = mutate(this.state);
186
+ this.persist();
187
+ return result;
188
+ }
189
+ catch (error) {
190
+ this.state = before;
191
+ throw error;
192
+ }
193
+ }
194
+ reminder(id) {
195
+ if (typeof id !== 'string' || !Object.hasOwn(this.state.reminders, id))
196
+ throw new ReminderError('not_found', 'no such reminder for this agent');
197
+ return this.state.reminders[id];
198
+ }
199
+ event(id) {
200
+ if (typeof id !== 'string' || !Object.hasOwn(this.state.events, id))
201
+ throw new ReminderError('not_found', 'no such reminder event for this agent');
202
+ return this.state.events[id];
203
+ }
204
+ schedule(input, previous) {
205
+ const title = input.title === undefined && previous ? previous.title : text(input.title, 'title', MAX_TITLE_CHARS, true);
206
+ const note = input.note === undefined && previous ? previous.note : text(input.note, 'note', MAX_NOTE_CHARS, false);
207
+ const retime = !previous || input.local_time !== undefined || input.time_zone !== undefined || input.fold !== undefined;
208
+ const late = input.late_window_seconds === undefined && previous
209
+ ? previous.lateWindowSeconds : lateWindow(input.late_window_seconds);
210
+ const interrupt = input.interrupt === undefined && previous
211
+ ? previous.interrupt === true : urgency(input.interrupt);
212
+ let when;
213
+ if (retime) {
214
+ try {
215
+ when = resolveSchedule(input.local_time ?? previous?.localTime, input.time_zone ?? previous?.timeZone, input.fold === undefined ? (input.local_time === undefined && input.time_zone === undefined ? previous?.fold : undefined) : input.fold);
216
+ }
217
+ catch (error) {
218
+ if (error instanceof ReminderScheduleError)
219
+ throw new ReminderError(error.code, error.message);
220
+ throw error;
221
+ }
222
+ const now = this.now();
223
+ if (when.dueAt <= now)
224
+ throw new ReminderError('not_in_future', 'the reminder time must be in the future');
225
+ if (when.dueAt - now > MAX_HORIZON_MS)
226
+ throw new ReminderError('too_far', 'the reminder time must be within five years');
227
+ }
228
+ else {
229
+ when = { localTime: previous.localTime, timeZone: previous.timeZone, fold: previous.fold, dueAt: Date.parse(previous.dueAt) };
230
+ }
231
+ return { title, note, ...when, late, interrupt };
232
+ }
233
+ openCount() {
234
+ return Object.values(this.state.reminders).filter(item => item.state === 'active' || item.state === 'delivered').length;
235
+ }
236
+ create(requestKey, input) {
237
+ const key = text(requestKey, 'request_key', MAX_REQUEST_KEY_CHARS, true);
238
+ const requestDigest = digest({
239
+ title: input.title ?? null, note: input.note ?? null, local_time: input.local_time ?? null,
240
+ time_zone: input.time_zone ?? null, fold: input.fold ?? null,
241
+ late_window_seconds: input.late_window_seconds ?? null,
242
+ // Only an explicit urgent request changes the digest, so a retry of a
243
+ // call made before `interrupt` existed (or with it false) still matches.
244
+ ...(input.interrupt === true ? { interrupt: true } : {}),
245
+ });
246
+ const existing = Object.values(this.state.reminders).find(item => item.requestKey === key);
247
+ if (existing) {
248
+ if (existing.requestDigest !== requestDigest)
249
+ throw new ReminderError('request_key_conflict', 'request_key was already used with different input');
250
+ return { reminder: this.view(existing), created: false };
251
+ }
252
+ if (this.openCount() >= MAX_OPEN_REMINDERS)
253
+ throw new ReminderError('limit_reached', `at most ${MAX_OPEN_REMINDERS} open reminders per agent`);
254
+ const values = this.schedule(input);
255
+ return this.commit(state => {
256
+ const at = iso(this.now());
257
+ const record = {
258
+ id: `rem_${randomUUID()}`, requestKey: key, requestDigest, revision: 1, state: 'active',
259
+ title: values.title, note: values.note, localTime: values.localTime, timeZone: values.timeZone,
260
+ ...(values.fold === undefined ? {} : { fold: values.fold }),
261
+ dueAt: iso(values.dueAt), lateWindowSeconds: values.late,
262
+ expiresAt: iso(values.dueAt + values.late * 1000), createdAt: at, updatedAt: at,
263
+ ...(values.interrupt ? { interrupt: true } : {}),
264
+ };
265
+ state.reminders[record.id] = record;
266
+ this.prune(state);
267
+ return { reminder: this.view(record), created: true };
268
+ });
269
+ }
270
+ update(id, expectedRevision, input) {
271
+ const record = this.reminder(id);
272
+ const revision = revisionOf(expectedRevision);
273
+ if (record.revision !== revision)
274
+ throw new ReminderError('stale_revision', `reminder is at revision ${record.revision}`);
275
+ if (record.state !== 'active' && record.state !== 'delivered')
276
+ throw new ReminderError('closed', `reminder is ${record.state}`);
277
+ if (Object.values(input).every(value => value === undefined))
278
+ throw new ReminderError('invalid_request', 'nothing to update');
279
+ const values = this.schedule(input, record);
280
+ return this.commit(state => {
281
+ const target = state.reminders[record.id];
282
+ const at = iso(this.now());
283
+ this.supersedeEvents(state, target.id, 'superseded_by_update', at);
284
+ Object.assign(target, {
285
+ revision: target.revision + 1, state: 'active',
286
+ title: values.title, note: values.note, localTime: values.localTime, timeZone: values.timeZone,
287
+ dueAt: iso(values.dueAt), lateWindowSeconds: values.late,
288
+ expiresAt: iso(values.dueAt + values.late * 1000), updatedAt: at,
289
+ });
290
+ if (values.fold === undefined)
291
+ delete target.fold;
292
+ else
293
+ target.fold = values.fold;
294
+ if (values.interrupt)
295
+ target.interrupt = true;
296
+ else
297
+ delete target.interrupt;
298
+ delete target.closedReason;
299
+ return this.view(target);
300
+ });
301
+ }
302
+ cancel(id, expectedRevision) {
303
+ const record = this.reminder(id);
304
+ const revision = revisionOf(expectedRevision);
305
+ if (record.revision !== revision)
306
+ throw new ReminderError('stale_revision', `reminder is at revision ${record.revision}`);
307
+ if (record.state !== 'active' && record.state !== 'delivered')
308
+ throw new ReminderError('closed', `reminder is ${record.state}`);
309
+ return this.commit(state => {
310
+ const target = state.reminders[record.id];
311
+ const at = iso(this.now());
312
+ this.supersedeEvents(state, target.id, 'cancelled', at);
313
+ target.revision++;
314
+ target.state = 'cancelled';
315
+ target.closedReason = 'cancelled';
316
+ target.updatedAt = at;
317
+ this.prune(state);
318
+ return this.view(target);
319
+ });
320
+ }
321
+ list(filter = {}) {
322
+ const scope = filter.state ?? 'open';
323
+ if (!['open', 'closed', 'all'].includes(scope))
324
+ throw new ReminderError('invalid_request', "state must be 'open', 'closed' or 'all'");
325
+ const limit = filter.limit ?? 50;
326
+ if (typeof limit !== 'number' || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
327
+ throw new ReminderError('invalid_request', 'limit must be 1..100');
328
+ if (filter.after !== undefined && typeof filter.after !== 'string')
329
+ throw new ReminderError('invalid_request', 'after must be a reminder id from a previous page');
330
+ const open = (item) => item.state === 'active' || item.state === 'delivered';
331
+ const rows = Object.values(this.state.reminders)
332
+ .filter(item => scope === 'all' || (scope === 'open') === open(item))
333
+ .sort((a, b) => a.dueAt.localeCompare(b.dueAt) || a.id.localeCompare(b.id));
334
+ const start = filter.after ? rows.findIndex(item => item.id === filter.after) + 1 : 0;
335
+ if (filter.after && start === 0)
336
+ throw new ReminderError('invalid_request', 'after does not name a reminder in this listing');
337
+ const page = rows.slice(start, start + limit);
338
+ const more = start + limit < rows.length;
339
+ return {
340
+ reminders: page.map(item => this.view(item)),
341
+ more, ...(more ? { after: page.at(-1).id } : {}),
342
+ uncertain_events: Object.values(this.state.events).filter(event => event.state === 'uncertain').length,
343
+ };
344
+ }
345
+ inspect(eventId) {
346
+ const event = this.event(eventId);
347
+ const record = this.state.reminders[event.reminderId];
348
+ const now = this.now();
349
+ const current = Boolean(record && record.revision === event.revision);
350
+ const eligible = Boolean(record && current && (event.state === 'dispatched' || event.state === 'uncertain')
351
+ && (record.state === 'delivered' || record.state === 'active'));
352
+ return {
353
+ event: this.eventView(event),
354
+ reminder: record ? this.view(record) : undefined,
355
+ current, eligible,
356
+ late_by_ms: record ? Math.max(0, now - Date.parse(record.dueAt)) : undefined,
357
+ };
358
+ }
359
+ ack(eventId, expectedRevision) {
360
+ const event = this.event(eventId);
361
+ const revision = revisionOf(expectedRevision);
362
+ if (event.revision !== revision)
363
+ throw new ReminderError('stale_revision', `event belongs to revision ${event.revision}`);
364
+ if (event.state === 'acknowledged')
365
+ return { event: this.eventView(event), acknowledged: true, duplicate: true };
366
+ if (event.state !== 'dispatched' && event.state !== 'uncertain')
367
+ throw new ReminderError('not_acknowledgeable', `event is ${event.state}`);
368
+ const record = this.state.reminders[event.reminderId];
369
+ if (!record || record.revision !== event.revision)
370
+ throw new ReminderError('stale_revision', 'the reminder changed after this wake; this event is no longer current');
371
+ return this.commit(state => {
372
+ const target = state.events[event.id];
373
+ const at = iso(this.now());
374
+ target.state = 'acknowledged';
375
+ target.closedAt = at;
376
+ const owner = state.reminders[target.reminderId];
377
+ owner.state = 'acknowledged';
378
+ owner.closedReason = 'acknowledged';
379
+ owner.updatedAt = at;
380
+ this.prune(state);
381
+ return { event: this.eventView(target), acknowledged: true, duplicate: false };
382
+ });
383
+ }
384
+ // ── dispatch side (runner only) ────────────────────────────────────────────
385
+ /**
386
+ * Earliest instant the dispatcher has work, or undefined when idle.
387
+ * `live` names admissions still owned by a queued prompt in this process:
388
+ * their own completion re-arms the dispatcher, so they are not work here.
389
+ */
390
+ nextWorkAt(live = new Set()) {
391
+ let next;
392
+ const consider = (value) => { next = next === undefined ? value : Math.min(next, value); };
393
+ for (const record of Object.values(this.state.reminders)) {
394
+ if (record.state !== 'active')
395
+ continue;
396
+ const open = this.openEvent(record);
397
+ if (open && live.has(open.id))
398
+ continue;
399
+ if (!open)
400
+ consider(Date.parse(record.dueAt));
401
+ else if (open.state === 'pending')
402
+ consider(open.retryAt ? Date.parse(open.retryAt) : Date.parse(record.dueAt));
403
+ else if (open.state === 'admitting')
404
+ consider(Date.parse(record.dueAt));
405
+ if (!open || open.state === 'pending' || open.state === 'admitting')
406
+ consider(Date.parse(record.expiresAt));
407
+ }
408
+ return next;
409
+ }
410
+ openEvent(record) {
411
+ return Object.values(this.state.events).find(event => event.reminderId === record.id
412
+ && event.revision === record.revision && OPEN_EVENT_STATES.has(event.state));
413
+ }
414
+ /**
415
+ * Expire what is past its window and create a pending event for each due
416
+ * reminder. Returns the events ready to admit now, earliest due first.
417
+ */
418
+ sweep(live = new Set()) {
419
+ const now = this.now();
420
+ let changed = false;
421
+ const ready = [];
422
+ const next = structuredClone(this.state);
423
+ for (const record of Object.values(next.reminders).sort((a, b) => a.dueAt.localeCompare(b.dueAt))) {
424
+ if (record.state !== 'active')
425
+ continue;
426
+ const open = Object.values(next.events).find(event => event.reminderId === record.id
427
+ && event.revision === record.revision && OPEN_EVENT_STATES.has(event.state));
428
+ if (open && live.has(open.id))
429
+ continue;
430
+ if (open?.state === 'admitting') {
431
+ // No queued prompt in this process owns it (a write that would have
432
+ // returned it to pending failed). The release guard never committed
433
+ // a dispatch, so it is still undispatched: the same move as restart
434
+ // recovery, made here so it can neither strand nor spin.
435
+ open.state = 'pending';
436
+ delete open.promptId;
437
+ delete open.retryAt;
438
+ changed = true;
439
+ }
440
+ if (now >= Date.parse(record.expiresAt) && (!open || open.state === 'pending')) {
441
+ if (open) {
442
+ open.state = 'expired';
443
+ open.closedAt = iso(now);
444
+ }
445
+ record.state = 'expired';
446
+ record.closedReason = 'late_window_elapsed';
447
+ record.updatedAt = iso(now);
448
+ changed = true;
449
+ continue;
450
+ }
451
+ if (now < Date.parse(record.dueAt))
452
+ continue;
453
+ if (!open) {
454
+ const event = {
455
+ id: `rev_${randomUUID()}`, reminderId: record.id, revision: record.revision,
456
+ state: 'pending', createdAt: iso(now), attempts: 0,
457
+ };
458
+ next.events[event.id] = event;
459
+ changed = true;
460
+ ready.push(event);
461
+ }
462
+ else if (open.state === 'pending' && (!open.retryAt || Date.parse(open.retryAt) <= now)) {
463
+ ready.push(open);
464
+ }
465
+ }
466
+ if (changed) {
467
+ this.prune(next);
468
+ const before = this.state;
469
+ this.state = next;
470
+ try {
471
+ this.persist();
472
+ }
473
+ catch (error) {
474
+ this.state = before;
475
+ throw error;
476
+ }
477
+ }
478
+ return ready.map(event => structuredClone(this.state.events[event.id]));
479
+ }
480
+ /** Durable claim made before the prompt is queued. False: do not queue. */
481
+ markAdmitting(eventId) {
482
+ const event = this.state.events[eventId];
483
+ const record = event && this.state.reminders[event.reminderId];
484
+ const now = this.now();
485
+ if (!event || event.state !== 'pending' || !record || record.state !== 'active'
486
+ || record.revision !== event.revision || now < Date.parse(record.dueAt)
487
+ || now >= Date.parse(record.expiresAt))
488
+ return false;
489
+ return this.commit(() => {
490
+ event.state = 'admitting';
491
+ event.attempts++;
492
+ event.admittedAt = iso(this.now());
493
+ delete event.retryAt;
494
+ return true;
495
+ });
496
+ }
497
+ /** Record the session's queue receipt; never moves an event backwards. */
498
+ recordReceipt(eventId, promptId) {
499
+ const event = this.state.events[eventId];
500
+ if (!event || (event.state !== 'admitting' && event.state !== 'dispatched') || event.promptId)
501
+ return;
502
+ this.commit(() => { event.promptId = promptId; });
503
+ }
504
+ /**
505
+ * The release guard: called synchronously by the session at the moment the
506
+ * prompt would be handed to the model. It returns true only after
507
+ * `dispatched` is durably on disk; any doubt refuses the dispatch.
508
+ */
509
+ claimDispatch(eventId, stopping) {
510
+ const event = this.state.events[eventId];
511
+ const record = event && this.state.reminders[event.reminderId];
512
+ if (!event || event.state !== 'admitting')
513
+ return { dispatch: false, reason: 'event_not_admitting' };
514
+ if (stopping)
515
+ return { dispatch: false, reason: 'stopping' };
516
+ const at = this.now();
517
+ const close = (state, reason) => {
518
+ try {
519
+ this.commit(() => { event.state = state; event.reason = reason; event.closedAt = iso(at); });
520
+ }
521
+ catch { /* left admitting: recovery returns it to pending, still undispatched */ }
522
+ return { dispatch: false, reason };
523
+ };
524
+ if (!record || record.revision !== event.revision || record.state !== 'active')
525
+ return close('invalidated', 'stale_revision');
526
+ // The wall clock can step back after admission. Not due yet means not yet:
527
+ // the event stays undispatched and is admitted again once it is due.
528
+ if (at < Date.parse(record.dueAt))
529
+ return { dispatch: false, reason: 'not_due' };
530
+ if (at >= Date.parse(record.expiresAt)) {
531
+ try {
532
+ this.commit(() => {
533
+ event.state = 'expired';
534
+ event.reason = 'late_window_elapsed';
535
+ event.closedAt = iso(at);
536
+ record.state = 'expired';
537
+ record.closedReason = 'late_window_elapsed';
538
+ record.updatedAt = iso(at);
539
+ });
540
+ }
541
+ catch { /* undispatched either way */ }
542
+ return { dispatch: false, reason: 'expired' };
543
+ }
544
+ try {
545
+ this.commit(() => {
546
+ event.state = 'dispatched';
547
+ event.dispatchedAt = iso(at);
548
+ record.state = 'delivered';
549
+ record.updatedAt = iso(at);
550
+ });
551
+ }
552
+ catch {
553
+ return { dispatch: false, reason: 'dispatch_commit_failed' };
554
+ }
555
+ return { dispatch: true };
556
+ }
557
+ /**
558
+ * Whether this undispatched event may still ask to interrupt a busy turn: its
559
+ * reminder is urgent and current, and no interruption was ever requested
560
+ * for it (in this process or before a restart).
561
+ */
562
+ mayInterrupt(eventId) {
563
+ const event = this.state.events[eventId];
564
+ const record = event && this.state.reminders[event.reminderId];
565
+ return Boolean(event && record && record.interrupt === true && !event.interrupt
566
+ && (event.state === 'pending' || event.state === 'admitting')
567
+ && record.state === 'active' && record.revision === event.revision);
568
+ }
569
+ /**
570
+ * Durable mark made BEFORE the session is asked to cancel. False: do not
571
+ * interrupt. Once written it is never cleared, which is what bounds every
572
+ * urgent event to one interruption.
573
+ */
574
+ markInterruptRequested(eventId, target) {
575
+ if (!this.mayInterrupt(eventId) || this.state.events[eventId].state !== 'admitting')
576
+ return false;
577
+ if (!target.source || !target.promptId || !target.generation)
578
+ return false;
579
+ const event = this.state.events[eventId];
580
+ try {
581
+ this.commit(() => {
582
+ event.interrupt = {
583
+ requestedAt: iso(this.now()), outcome: 'requested',
584
+ target: {
585
+ source: target.source, promptId: target.promptId, generation: target.generation,
586
+ ...(target.sessionId ? { sessionId: target.sessionId } : {}),
587
+ },
588
+ };
589
+ delete event.interruptDeferred;
590
+ });
591
+ }
592
+ catch {
593
+ return false;
594
+ }
595
+ return true;
596
+ }
597
+ /**
598
+ * Record why an urgent event did not interrupt this time. Written only when
599
+ * the reason changes, so a long busy stretch costs one write, not one per poll.
600
+ */
601
+ recordInterruptDeferred(eventId, reason) {
602
+ const event = this.state.events[eventId];
603
+ if (!event || event.interrupt || event.interruptDeferred === reason)
604
+ return;
605
+ try {
606
+ this.commit(() => { event.interruptDeferred = reason; });
607
+ }
608
+ catch { /* diagnostic only */ }
609
+ }
610
+ /** How the requested interruption ended. Diagnostic; never moves the event. */
611
+ recordInterruptOutcome(eventId, outcome, reason) {
612
+ const event = this.state.events[eventId];
613
+ if (!event?.interrupt || event.interrupt.outcome !== 'requested')
614
+ return;
615
+ try {
616
+ this.commit(() => {
617
+ event.interrupt = {
618
+ ...event.interrupt, outcome, settledAt: iso(this.now()), ...(reason ? { reason } : {}),
619
+ };
620
+ });
621
+ }
622
+ catch { /* stays `requested`: still never re-requested, still not delivered */ }
623
+ }
624
+ /** Still the current, undispatched claim this process made? Re-checked after every await. */
625
+ stillAdmitting(eventId) {
626
+ const event = this.state.events[eventId];
627
+ const record = event && this.state.reminders[event.reminderId];
628
+ return Boolean(event && record && event.state === 'admitting' && record.state === 'active'
629
+ && record.revision === event.revision);
630
+ }
631
+ /**
632
+ * An admission that ended without the release guard dispatching it (queue
633
+ * refused, session died, release refused while stopping) goes back to
634
+ * pending. Anything already past `admitting` is left exactly as it is.
635
+ */
636
+ releaseUndispatched(eventId, retryAt) {
637
+ const event = this.state.events[eventId];
638
+ if (!event || event.state !== 'admitting')
639
+ return;
640
+ try {
641
+ this.commit(() => {
642
+ event.state = 'pending';
643
+ event.retryAt = iso(retryAt);
644
+ delete event.promptId;
645
+ });
646
+ }
647
+ catch { /* stays admitting; restart recovery makes the same move */ }
648
+ }
649
+ eventSummary(eventId) {
650
+ const event = this.state.events[eventId];
651
+ const reminder = event && this.state.reminders[event.reminderId];
652
+ return event && reminder ? structuredClone({ event, reminder }) : undefined;
653
+ }
654
+ supersedeEvents(state, reminderId, reason, at) {
655
+ for (const event of Object.values(state.events))
656
+ if (event.reminderId === reminderId && OPEN_EVENT_STATES.has(event.state)) {
657
+ event.state = 'invalidated';
658
+ event.reason = reason;
659
+ event.closedAt = at;
660
+ }
661
+ }
662
+ /** Bound retained history; open reminders and their events are never dropped. */
663
+ prune(state) {
664
+ const closed = Object.values(state.reminders)
665
+ .filter(item => item.state !== 'active' && item.state !== 'delivered'
666
+ && !Object.values(state.events).some(event => event.reminderId === item.id && event.state === 'uncertain'))
667
+ .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
668
+ for (const item of closed.slice(MAX_RETAINED_CLOSED)) {
669
+ delete state.reminders[item.id];
670
+ for (const [id, event] of Object.entries(state.events))
671
+ if (event.reminderId === item.id)
672
+ delete state.events[id];
673
+ }
674
+ }
675
+ view(record) {
676
+ const event = Object.values(this.state.events)
677
+ .filter(item => item.reminderId === record.id && item.revision === record.revision)
678
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0];
679
+ return {
680
+ id: record.id, request_key: record.requestKey, revision: record.revision, state: record.state,
681
+ title: record.title, note: record.note,
682
+ local_time: record.localTime, time_zone: record.timeZone,
683
+ ...(record.fold === undefined ? {} : { fold: record.fold }),
684
+ due_at: record.dueAt, late_window_seconds: record.lateWindowSeconds, interrupt: record.interrupt === true,
685
+ expires_at: record.expiresAt,
686
+ created_at: record.createdAt, updated_at: record.updatedAt,
687
+ ...(record.closedReason ? { closed_reason: record.closedReason } : {}),
688
+ ...(event ? { event: {
689
+ id: event.id, state: event.state, revision: event.revision,
690
+ ...(event.interrupt ? { interrupt_state: event.interrupt.outcome } : {}),
691
+ } } : {}),
692
+ };
693
+ }
694
+ eventView(event) {
695
+ return {
696
+ id: event.id, reminder_id: event.reminderId, revision: event.revision, state: event.state,
697
+ created_at: event.createdAt,
698
+ ...(event.dispatchedAt ? { dispatched_at: event.dispatchedAt } : {}),
699
+ ...(event.closedAt ? { closed_at: event.closedAt } : {}),
700
+ ...(event.reason ? { reason: event.reason } : {}),
701
+ ...(event.interruptDeferred ? { interrupt_deferred: event.interruptDeferred } : {}),
702
+ ...(event.interrupt ? { interrupt: {
703
+ state: event.interrupt.outcome, requested_at: event.interrupt.requestedAt,
704
+ target_source: event.interrupt.target.source,
705
+ target: {
706
+ source: event.interrupt.target.source, prompt_id: event.interrupt.target.promptId,
707
+ generation: event.interrupt.target.generation,
708
+ ...(event.interrupt.target.sessionId ? { session_id: event.interrupt.target.sessionId } : {}),
709
+ },
710
+ ...(event.interrupt.settledAt ? { settled_at: event.interrupt.settledAt } : {}),
711
+ ...(event.interrupt.reason ? { reason: event.interrupt.reason } : {}),
712
+ } } : {}),
713
+ };
714
+ }
715
+ }
716
+ function sameOwner(a, b) {
717
+ if (a.role !== b.role || a.lifetime !== b.lifetime || a.cid.toUpperCase() !== b.cid.toUpperCase())
718
+ return false;
719
+ return a.lifetime === 'permanent' || (a.instance === b.instance && a.action === b.action);
720
+ }
721
+ function privateFile(path) {
722
+ const stat = lstatSync(path);
723
+ const uid = process.getuid?.();
724
+ return stat.isFile() && !stat.isSymbolicLink() && (stat.mode & 0o077) === 0
725
+ && (uid === undefined || stat.uid === uid);
726
+ }
727
+ function validFile(value) {
728
+ if (!value || typeof value !== 'object')
729
+ return false;
730
+ const file = value;
731
+ const owner = file.owner;
732
+ if (file.version !== 1 || !owner || typeof owner !== 'object'
733
+ || !['role', 'instance', 'cid', 'action'].every(key => typeof owner[key] === 'string')
734
+ || (owner.lifetime !== 'permanent' && owner.lifetime !== 'temporary')
735
+ || !file.reminders || typeof file.reminders !== 'object' || !file.events || typeof file.events !== 'object')
736
+ return false;
737
+ const reminderStates = new Set(['active', 'delivered', 'acknowledged', 'cancelled', 'expired']);
738
+ const eventStates = new Set(['pending', 'admitting', 'dispatched', 'uncertain', 'acknowledged', 'invalidated', 'expired']);
739
+ return Object.entries(file.reminders).every(([id, item]) => item && item.id === id
740
+ && Number.isSafeInteger(item.revision) && item.revision >= 1 && reminderStates.has(item.state)
741
+ && typeof item.requestKey === 'string' && typeof item.title === 'string' && typeof item.note === 'string'
742
+ && Number.isFinite(Date.parse(item.dueAt)) && Number.isFinite(Date.parse(item.expiresAt))
743
+ && (item.interrupt === undefined || item.interrupt === true))
744
+ && Object.entries(file.events).every(([id, item]) => item && item.id === id
745
+ && typeof item.reminderId === 'string' && Number.isSafeInteger(item.revision) && eventStates.has(item.state)
746
+ && Number.isSafeInteger(item.attempts)
747
+ && (item.interrupt === undefined || (typeof item.interrupt === 'object' && item.interrupt !== null
748
+ && typeof item.interrupt.requestedAt === 'string' && typeof item.interrupt.target?.source === 'string'
749
+ && typeof item.interrupt.target.promptId === 'string' && typeof item.interrupt.target.generation === 'string'
750
+ && (item.interrupt.target.sessionId === undefined || typeof item.interrupt.target.sessionId === 'string')
751
+ && INTERRUPT_OUTCOMES.has(item.interrupt.outcome))));
752
+ }
753
+ /** Remove a retired agent's reminder store and any quarantined copies. */
754
+ export function removeReminderState(stateDir) {
755
+ rmSync(join(stateDir, SELF_REMINDERS_FILE), { force: true });
756
+ try {
757
+ for (const name of readdirSync(stateDir))
758
+ if (name.startsWith(`${SELF_REMINDERS_FILE}.`) || name.startsWith(`.${SELF_REMINDERS_FILE}.`))
759
+ rmSync(join(stateDir, name), { force: true });
760
+ }
761
+ catch { /* directory already gone */ }
762
+ }