@humwick/server 0.130.0 → 0.130.2

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 (486) hide show
  1. package/README.md +5 -0
  2. package/docs/guides/tools-administration.md +6 -1
  3. package/docs/guides/tools-coordination.md +1 -0
  4. package/docs/guides/tools-tasks.md +8 -0
  5. package/docs/guides/workflows.md +58 -0
  6. package/docs/openapi.json +414 -1
  7. package/package.json +8 -17
  8. package/public/assets/{MessageBubble-BWpoBjl4.js → MessageBubble-BWLqMGKG.js} +2 -2
  9. package/public/assets/{PlanGraphPanel-C0auck3n.js → PlanGraphPanel-3TlIQpjk.js} +2 -2
  10. package/public/assets/{TraceView-DBnOx7AQ.js → TraceView-Wimm0Czv.js} +2 -2
  11. package/public/assets/{VaultDecisionCard-0kp2oNBC.js → VaultDecisionCard-Bau_vNax.js} +2 -2
  12. package/public/assets/{activity-yLztXEBt.js → activity-CrSurn-r.js} +2 -2
  13. package/public/assets/{agent-lineage-BbJDR1dW.js → agent-lineage-CknAJKcb.js} +2 -2
  14. package/public/assets/agent-liveness-CaNwK0Zf.js +2 -0
  15. package/public/assets/{agent-name-CTZBrSD9.js → agent-name-DKbiIhBo.js} +2 -2
  16. package/public/assets/agent-profiles-DwF6EQ4R.js +2 -0
  17. package/public/assets/{agent-project-badge-9TqzUUYE.js → agent-project-badge-Diz4Sjjb.js} +2 -2
  18. package/public/assets/{agent-tools-D4n40pn8.js → agent-tools-CRhpRQza.js} +2 -2
  19. package/public/assets/{agent-type-icon-D6FUarOf.js → agent-type-icon-LrUEU3lB.js} +2 -2
  20. package/public/assets/{agents-BYDslkJc.js → agents-B-v66C-4.js} +2 -2
  21. package/public/assets/{analytics-B-XSbEMC.js → analytics-BqCb-LKi.js} +2 -2
  22. package/public/assets/{artifact-viewer-DdWIfV6M.js → artifact-viewer-kf4pGuAR.js} +2 -2
  23. package/public/assets/{artifacts-oBOqks-2.js → artifacts-D32zyw4Y.js} +2 -2
  24. package/public/assets/{artifacts-DPN1q4pA.js → artifacts-DJevXcvg.js} +2 -2
  25. package/public/assets/{automation-4KcRFXEu.js → automation-BpRajx_5.js} +2 -2
  26. package/public/assets/{backlog-Cw9xPTCF.js → backlog-D_UjTdzX.js} +2 -2
  27. package/public/assets/{busy-health-CTu32MZt.js → busy-health-N3VEh4ao.js} +2 -2
  28. package/public/assets/{channels-BiXzg1G9.js → channels-9jopungq.js} +2 -2
  29. package/public/assets/{chat-Ctk2qwvA.js → chat-CSXrTB3C.js} +2 -2
  30. package/public/assets/{code-preview-Bm1wWqAp.js → code-preview-p1NnmqK0.js} +2 -2
  31. package/public/assets/{connectors-C36ax4Vw.js → connectors-CZdqlDNL.js} +2 -2
  32. package/public/assets/{cosmos-Jj9LK2Wf.js → cosmos-BgdsE4dG.js} +2 -2
  33. package/public/assets/{crate-agent-H6S7-B9T.js → crate-agent-30aXTKva.js} +2 -2
  34. package/public/assets/{dependency-graph-Cd1E23wa.js → dependency-graph-B_jvw2GE.js} +2 -2
  35. package/public/assets/{deploys-Bwfq146q.js → deploys-DoN6bAb5.js} +2 -2
  36. package/public/assets/{display-CfO7rTuF.js → display-BrKWFGJz.js} +2 -2
  37. package/public/assets/{files-DhhZI2Ih.js → files-D6XEu8Ng.js} +2 -2
  38. package/public/assets/fleet-C7jQWbw2.js +2 -0
  39. package/public/assets/{fleet-metrics-BkHP-P3R.js → fleet-metrics-BiDs3MLx.js} +2 -2
  40. package/public/assets/{formatted-body-Cm1RrzmN.js → formatted-body-CQBwc1fb.js} +3 -3
  41. package/public/assets/{formatted-body-impl-DQiOuOD1.js → formatted-body-impl-DyG0WsQS.js} +2 -2
  42. package/public/assets/{host-providers-dialog-CHqwXTov.js → host-providers-dialog-DKaW31dN.js} +2 -2
  43. package/public/assets/index-CRHep3M9.js +15 -0
  44. package/public/assets/index-MYAIwYTX.css +2 -0
  45. package/public/assets/{insights-fH1EL-qZ.js → insights-CzrtkpOE.js} +2 -2
  46. package/public/assets/{integrations-C5Oix6ng.js → integrations-aVsUmigt.js} +2 -2
  47. package/public/assets/{issue-ref-chip-ODyondIa.js → issue-ref-chip-BnSlM_95.js} +2 -2
  48. package/public/assets/{log-viewer-Cvfy6PW7.js → log-viewer-qe7Ee47Z.js} +2 -2
  49. package/public/assets/{maintenance-D8CDH5wZ.js → maintenance-NfjqFZAk.js} +2 -2
  50. package/public/assets/{managed-agents-egllbpij.js → managed-agents-BiH0HgaE.js} +2 -2
  51. package/public/assets/{markdown-preview-CYSFgVgK.js → markdown-preview-BixaT3Xr.js} +3 -3
  52. package/public/assets/{markdown-preview-impl-BteVIjX4.js → markdown-preview-impl-DNGlWIS3.js} +2 -2
  53. package/public/assets/{memory-C8WmXsyr.js → memory-DNsCl6UX.js} +2 -2
  54. package/public/assets/{messages-BzQdqlO6.js → messages-DopU1L1X.js} +2 -2
  55. package/public/assets/{orchestrators-B1HxzkNw.js → orchestrators-Cq7ckLQT.js} +2 -2
  56. package/public/assets/{overview-DCUnI4Z7.js → overview-C1DCTzzV.js} +2 -2
  57. package/public/assets/{pairs-ftCd2XcD.js → pairs-DE_m6rrE.js} +2 -2
  58. package/public/assets/{profile-icon-B2zH8DMH.js → profile-icon-B3rfh77F.js} +2 -2
  59. package/public/assets/project-profile-instructions-DQWZE0kA.js +2 -0
  60. package/public/assets/projects-Br2Bd2h1.js +2 -0
  61. package/public/assets/{prompt-settings-Bftq9czX.js → prompt-settings-BKvFsvGD.js} +2 -2
  62. package/public/assets/provider-display-DsuWMvVL.js +2 -0
  63. package/public/assets/{registry-DWCU5c8q.js → registry-Cw38_6gW.js} +2 -2
  64. package/public/assets/run-card-CpKqRbhg.js +2 -0
  65. package/public/assets/scroll-text-BKXS1ahr.js +2 -0
  66. package/public/assets/{secrets-C7GIUATz.js → secrets-DxI-vzSq.js} +2 -2
  67. package/public/assets/{service-access-settings-C3aujqTG.js → service-access-settings-D9s_VJbd.js} +2 -2
  68. package/public/assets/{settings-C6sZ9eBa.js → settings-CKZTVGIx.js} +2 -2
  69. package/public/assets/{shipping-Bu8p1gEw.js → shipping-B9YtnwtG.js} +2 -2
  70. package/public/assets/{stats-V9Z-GxUd.js → stats-DyUBKyWI.js} +2 -2
  71. package/public/assets/{status-dot-CVv0iGHL.js → status-dot-DYqlEUhJ.js} +2 -2
  72. package/public/assets/{store-BvtFdlGX.js → store-DcClXLH0.js} +3 -3
  73. package/public/assets/{tasks-CXU75HRF.js → tasks-Bh7Chr4-.js} +2 -2
  74. package/public/assets/{teams-BPf976zb.js → teams-bowI7qVY.js} +2 -2
  75. package/public/assets/{terminal-viewer-DE9BOe-8.js → terminal-viewer-BgVLHdsU.js} +3 -3
  76. package/public/assets/{terminal-viewer-impl-D04pjmGh.js → terminal-viewer-impl-Dz22UPy6.js} +2 -2
  77. package/public/assets/token-estimate-Co-Jdphh.js +2 -0
  78. package/public/assets/{use-agent-terminal-B1yR456R.js → use-agent-terminal-DSQQ-iY7.js} +2 -2
  79. package/public/assets/{use-decisions-D-S4tpDf.js → use-decisions-samMQhQf.js} +2 -2
  80. package/public/assets/use-project-list-59LheVP7.js +2 -0
  81. package/public/assets/{use-selectors-CnrAk2YP.js → use-selectors-BZa25xEB.js} +2 -2
  82. package/public/assets/{user-avatar-nBFIcR4M.js → user-avatar-DHxlvUvV.js} +2 -2
  83. package/public/assets/versioned-prompt-editor-BmED5DnJ.js +8 -0
  84. package/public/assets/{work-queue-TCjJxeT-.js → work-queue-CSvpuNLz.js} +2 -2
  85. package/public/assets/{workflow-marker-Cf3Hf2fN.js → workflow-marker-DAZcj5qt.js} +2 -2
  86. package/public/assets/{workflows-CXQX20Qj.js → workflows-DdmxRSyg.js} +2 -2
  87. package/public/assets/{workspaces-qSYzKYMA.js → workspaces-BxeKfVuF.js} +2 -2
  88. package/public/cosmos.html +4 -4
  89. package/public/index.html +31 -29
  90. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  91. package/runner/plugins/claude/hooks/gate-run-guard.provisioned.mjs +31 -1
  92. package/runner/src/adapters/claude-effort.ts +2 -4
  93. package/runner/src/adapters/codex-launch-config.ts +4 -5
  94. package/runner/src/adapters/codex.ts +1 -1
  95. package/runner/src/adapters/opencode-provider.ts +7 -22
  96. package/runner/src/adapters/opencode.ts +4 -4
  97. package/runner/src/agent-runner/credentials.ts +42 -12
  98. package/runner/src/agent-runner/inbox.ts +27 -0
  99. package/runner/src/agent-runner/lifecycle-milestones.ts +0 -2
  100. package/runner/src/bin/claude.ts +4 -0
  101. package/runner/src/bin/codex.ts +4 -0
  102. package/runner/src/bin/opencode.ts +4 -0
  103. package/runner/src/index.ts +5 -2
  104. package/runner/src/provider-setup.ts +34 -0
  105. package/runner/src/providers.ts +2 -1
  106. package/runner/src/runner-core.ts +18 -23
  107. package/runner/src/spawn-input.ts +2 -1
  108. package/runner/src/test-run-dedup.ts +59 -56
  109. package/scripts/cd-config.ts +24 -3
  110. package/scripts/gate-run-lease.ts +10 -7
  111. package/scripts/gate.sh +5 -1
  112. package/src/branch-land-notice.ts +93 -0
  113. package/src/branch-landed.ts +7 -76
  114. package/src/bus.ts +19 -20
  115. package/src/cd-deploy-notify.ts +30 -2
  116. package/src/cd-host-lag-notify.ts +7 -6
  117. package/src/ci/incidents.ts +2 -2
  118. package/src/ci/monitor.ts +5 -8
  119. package/src/ci/root-missing.ts +2 -7
  120. package/src/ci/transition-notify.ts +7 -39
  121. package/src/cli/cd-pauses.ts +19 -0
  122. package/src/cli/orchestrator.ts +1 -1
  123. package/src/cli/state.ts +2 -19
  124. package/src/cli/task-edit.ts +22 -0
  125. package/src/cli/task.ts +3 -1
  126. package/src/cli/workflow.ts +8 -2
  127. package/src/commands-db.ts +10 -6
  128. package/src/commands-write.ts +8 -0
  129. package/src/config-store-write.ts +13 -1
  130. package/src/config-store.ts +25 -14
  131. package/src/db/activity-async.ts +6 -1
  132. package/src/db/activity-write-ops.ts +13 -1
  133. package/src/db/activity.ts +1 -14
  134. package/src/db/agent-lifecycle-write-ops.ts +1 -0
  135. package/src/db/agent-meta.ts +2 -2
  136. package/src/db/agent-ownership-write-ops.ts +4 -0
  137. package/src/db/agent-search.ts +1 -15
  138. package/src/db/agents-async.ts +58 -7
  139. package/src/db/agents-write-ops.ts +28 -5
  140. package/src/db/agents.ts +81 -54
  141. package/src/db/artifacts-write-ops.ts +8 -3
  142. package/src/db/bus-outbox-write-ops.ts +1 -0
  143. package/src/db/channels-write-ops.ts +4 -0
  144. package/src/db/ci-incidents-async.ts +5 -5
  145. package/src/db/ci-incidents-write-ops.ts +2 -2
  146. package/src/db/claude-api-budget-ops.ts +5 -5
  147. package/src/db/claude-app-oauth.ts +4 -4
  148. package/src/db/codex-reset-consumes-write-ops.ts +3 -0
  149. package/src/db/command-row.ts +10 -2
  150. package/src/db/commands-list-query.ts +2 -0
  151. package/src/db/commands-spawn-read.ts +1 -1
  152. package/src/db/commands-write-ops.ts +21 -0
  153. package/src/db/config-store-write-ops.ts +9 -2
  154. package/src/db/continuation-archives-async.ts +5 -0
  155. package/src/db/continuation-archives-write-ops.ts +9 -0
  156. package/src/db/continuations.ts +2 -2
  157. package/src/db/db-writer-worker.ts +8 -1
  158. package/src/db/delivery-write-ops.ts +5 -0
  159. package/src/db/deploy-events-async.ts +1 -0
  160. package/src/db/deploy-events-read-ops.ts +2 -1
  161. package/src/db/deploy-events.ts +35 -4
  162. package/src/db/hosts-registration.ts +10 -2
  163. package/src/db/hosts-write-ops.ts +14 -0
  164. package/src/db/hosts.ts +9 -6
  165. package/src/db/inbox-write-ops.ts +4 -0
  166. package/src/db/inbox.ts +0 -14
  167. package/src/db/index.ts +2 -0
  168. package/src/db/insights-write-ops.ts +1 -0
  169. package/src/db/integrations-async.ts +8 -0
  170. package/src/db/integrations-ops.ts +15 -0
  171. package/src/db/integrations.ts +15 -21
  172. package/src/db/issue-lifecycle.ts +3 -4
  173. package/src/db/issue-refs-async.ts +20 -0
  174. package/src/db/issue-refs-write-ops.ts +29 -0
  175. package/src/db/issue-refs.ts +7 -8
  176. package/src/db/long-running-work.ts +2 -2
  177. package/src/db/macos-privacy.ts +1 -1
  178. package/src/db/main-connection-writes.ts +23 -2
  179. package/src/db/maintenance-job-reconcile.ts +61 -0
  180. package/src/db/maintenance-job-runs-async.ts +14 -0
  181. package/src/db/maintenance-job-write-ops.ts +9 -0
  182. package/src/db/mappers.ts +2 -14
  183. package/src/db/merge-lease.ts +1 -15
  184. package/src/db/message-reads-write-ops.ts +3 -0
  185. package/src/db/message-write-ops.ts +4 -0
  186. package/src/db/migrations.ts +7 -15
  187. package/src/db/orchestrators-async.ts +7 -3
  188. package/src/db/orchestrators-write-ops.ts +7 -1
  189. package/src/db/orchestrators.ts +21 -77
  190. package/src/db/pairs.ts +1 -13
  191. package/src/db/project-profile-instructions.ts +50 -0
  192. package/src/db/prompt-versions-ops.ts +1 -0
  193. package/src/db/prompt-versions.ts +9 -2
  194. package/src/db/provider-catalog-write-ops.ts +4 -4
  195. package/src/db/provider-config-history.ts +8 -3
  196. package/src/db/provider-flap-messages-write-ops.ts +1 -0
  197. package/src/db/provider-model-seed.ts +34 -0
  198. package/src/db/provider-quota-lease-write-ops.ts +1 -0
  199. package/src/db/provider-quota-write-ops.ts +2 -0
  200. package/src/db/provisioning-write-ops.ts +9 -8
  201. package/src/db/read-op-marshaling-fixtures.ts +5 -0
  202. package/src/db/read-ops.ts +4 -0
  203. package/src/db/routing-policy.ts +3 -4
  204. package/src/db/schema.ts +9 -3
  205. package/src/db/secrets-broker-write-ops.ts +5 -5
  206. package/src/db/secrets-vault-write.ts +7 -7
  207. package/src/db/services-write-ops.ts +3 -0
  208. package/src/db/spawn-delegation-state.ts +11 -4
  209. package/src/db/spawn-tree-cleanup.ts +3 -0
  210. package/src/db/stats-async.ts +4 -3
  211. package/src/db/task-entity.ts +23 -2
  212. package/src/db/task-list.ts +3 -1
  213. package/src/db/tasks.ts +1 -17
  214. package/src/db/team-spawn-delegation.ts +1 -1
  215. package/src/db/teams-write-ops.ts +4 -4
  216. package/src/db/timer-cutover-write-ops.ts +7 -0
  217. package/src/db/timer-ingress-write-ops.ts +9 -0
  218. package/src/db/timer-ingress.ts +7 -1
  219. package/src/db/timer-operator-write-ops.ts +14 -0
  220. package/src/db/timer-transfer-write-ops.ts +4 -0
  221. package/src/db/timer-write-ops.ts +14 -0
  222. package/src/db/token-db-write-ops.ts +21 -0
  223. package/src/db/token-standing.ts +15 -11
  224. package/src/db/user-settings-write-ops.ts +1 -0
  225. package/src/db/vault-backup.ts +2 -2
  226. package/src/db/vault-expiry.ts +2 -2
  227. package/src/db/vault-health.ts +3 -3
  228. package/src/db/vault-token-ops.ts +8 -8
  229. package/src/db/web-tool-call-ops.ts +3 -3
  230. package/src/db/workflow-contact.ts +1 -1
  231. package/src/db/workflow-task-read-ops.ts +30 -0
  232. package/src/db/write-ops.ts +15 -1
  233. package/src/db/writer-fault-seam.ts +5 -2
  234. package/src/db/writer-gateway.ts +21 -12
  235. package/src/dispatch-availability.ts +10 -6
  236. package/src/ephemeral/{landing-ingress-proof-worker.ts → landing-ingress-proof.test-fixture.ts} +4 -2
  237. package/src/host/disk-alert.ts +121 -0
  238. package/src/lifecycle-manager.ts +3 -2
  239. package/src/maintenance/cd-watcher.ts +3 -3
  240. package/src/maintenance/critical-job-watchdog.ts +16 -17
  241. package/src/maintenance/index.ts +1 -0
  242. package/src/maintenance/jobs.ts +3 -2
  243. package/src/maintenance/scheduler.ts +84 -128
  244. package/src/maintenance/stalled-delivery-pager.ts +6 -5
  245. package/src/maintenance/workspaces.ts +1 -1
  246. package/src/managed-policy.ts +31 -27
  247. package/src/mcp/index.ts +5 -2
  248. package/src/mcp/output-descriptions.ts +8 -8
  249. package/src/mcp/presentation.ts +4 -4
  250. package/src/mcp/project-profile-instructions.ts +32 -0
  251. package/src/mcp/tool-groups.ts +1 -1
  252. package/src/mcp/tools-spawn.ts +4 -3
  253. package/src/mcp-agent-action.ts +1 -2
  254. package/src/mcp-host-tools.ts +1 -1
  255. package/src/mcp-issue-ref-tools.ts +12 -11
  256. package/src/mcp-maintenance-tools.ts +2 -2
  257. package/src/mcp-profile-trust-tools.ts +4 -4
  258. package/src/mcp-provider-catalog.ts +2 -2
  259. package/src/mcp-task-tools.ts +11 -5
  260. package/src/mcp-team-template-tools.ts +3 -3
  261. package/src/mcp-workflow-tools.ts +10 -3
  262. package/src/notification-types.ts +2 -1
  263. package/src/operator-alarm.ts +26 -11
  264. package/src/package-pack.ts +42 -39
  265. package/src/project-instructions.ts +4 -0
  266. package/src/project-profile-instruction-text.ts +6 -0
  267. package/src/project-profile-instructions.ts +20 -0
  268. package/src/provider-catalog-store.ts +3 -49
  269. package/src/provider-config-store.ts +12 -11
  270. package/src/provider-model-seed.ts +1 -18
  271. package/src/provider-quota-config-store.ts +5 -4
  272. package/src/provider-secrets.test-fixture.ts +3 -3
  273. package/src/recipe-runner.ts +2 -2
  274. package/src/repo-config-source.ts +10 -4
  275. package/src/routes/agent-profiles.ts +6 -6
  276. package/src/routes/agents-spawn.ts +1 -1
  277. package/src/routes/agents.ts +6 -6
  278. package/src/routes/commands-listing.ts +4 -0
  279. package/src/routes/commands.ts +2 -2
  280. package/src/routes/continuation-archives.ts +2 -2
  281. package/src/routes/host-resource-control.ts +4 -1
  282. package/src/routes/index.ts +10 -1
  283. package/src/routes/integrations.ts +24 -50
  284. package/src/routes/issue-refs.ts +7 -7
  285. package/src/routes/orchestrator-bootstrap.ts +3 -3
  286. package/src/routes/orchestrator-host-registration.ts +2 -2
  287. package/src/routes/orchestrator-host-utilization.ts +3 -1
  288. package/src/routes/orchestrator-proxy.ts +2 -2
  289. package/src/routes/orchestrator.ts +4 -4
  290. package/src/routes/project-profile-instructions.ts +27 -0
  291. package/src/routes/project-trust.ts +2 -2
  292. package/src/routes/provider-config.ts +9 -8
  293. package/src/routes/provider-quota-config.ts +2 -2
  294. package/src/routes/settings.ts +5 -5
  295. package/src/routes/stats.ts +6 -6
  296. package/src/routes/task-http.ts +15 -1
  297. package/src/routes/task-issues.ts +11 -26
  298. package/src/routes/tasks.ts +22 -53
  299. package/src/routes/team-templates.ts +5 -5
  300. package/src/routes/teams-config.ts +2 -2
  301. package/src/routes/tokens.ts +12 -5
  302. package/src/routes/workflows.ts +20 -5
  303. package/src/routing-policy-normalize.ts +1 -2
  304. package/src/routing-step-defaults.ts +0 -2
  305. package/src/runner-token-mint-limit.ts +63 -0
  306. package/src/runtime-grant.ts +4 -2
  307. package/src/runtime-tokens.ts +50 -9
  308. package/src/security.ts +13 -20
  309. package/src/server-background.ts +1 -1
  310. package/src/server.ts +5 -2
  311. package/src/services/host-utilization.ts +23 -0
  312. package/src/services/idle-epics.ts +5 -6
  313. package/src/services/issue-lifecycle.ts +57 -61
  314. package/src/services/issue-refs.ts +9 -13
  315. package/src/services/kernel-timers-cutover-chaos-worker.ts +12 -7
  316. package/src/services/kernel-timers-ingress.ts +1 -1
  317. package/src/services/kernel-timers-request-key-migration-chaos-worker.ts +4 -1
  318. package/src/services/landed-ancestry.ts +19 -3
  319. package/src/services/memory-areas.ts +15 -51
  320. package/src/services/plan-ci-notice.ts +57 -0
  321. package/src/services/plan-ci-recovery.ts +2 -55
  322. package/src/services/plan-wake-digest.ts +1 -0
  323. package/src/services/profile-trust.ts +4 -3
  324. package/src/services/project-config-read.ts +70 -0
  325. package/src/services/project-profile-instructions.ts +54 -0
  326. package/src/services/provisioning-harness/budget-baseline.json +4 -4
  327. package/src/services/restart-spawn.ts +61 -49
  328. package/src/services/self-care-state.ts +2 -1
  329. package/src/services/self-effort.ts +5 -5
  330. package/src/services/send-message.ts +5 -1
  331. package/src/services/spawn-agent.ts +13 -2
  332. package/src/services/spawn-chain-inspector.ts +6 -13
  333. package/src/services/spawn-policy.ts +68 -0
  334. package/src/services/spawn-tree-cleanup.ts +2 -2
  335. package/src/services/task-dispatch.ts +3 -0
  336. package/src/services/task-state-reconcile.ts +6 -14
  337. package/src/services/team-templates.ts +9 -8
  338. package/src/services/transient-agent-reaper.ts +3 -1
  339. package/src/services/workflow-auto-start.ts +25 -13
  340. package/src/services/workflow-ci-main.ts +1 -1
  341. package/src/services/workflow-deferred-cleanup.ts +4 -2
  342. package/src/services/workflow-effect-driver.ts +2 -0
  343. package/src/services/workflow-effects.ts +18 -11
  344. package/src/services/workflow-land-notice.ts +10 -0
  345. package/src/services/workflow-pause-text.ts +43 -0
  346. package/src/services/workflow-reconcile.ts +13 -11
  347. package/src/services/workflow-run-view.ts +2 -0
  348. package/src/services/workflow-runs.ts +29 -13
  349. package/src/services/workflow-supervisor-state.ts +5 -0
  350. package/src/services/workflow-supervisor.ts +7 -3
  351. package/src/settings/registry.ts +1 -2
  352. package/src/spawn-command.ts +4 -4
  353. package/src/spawn-models.ts +10 -12
  354. package/src/spawn-placement.ts +1 -1
  355. package/src/spawn-targets.ts +1 -1
  356. package/src/startup-context-injection.ts +1 -0
  357. package/src/state-adoption/cd-cutover.ts +53 -7
  358. package/src/state-adoption/fleet-cutover.ts +35 -0
  359. package/src/state-adoption/fleet-plan.ts +36 -0
  360. package/src/state-adoption/index.ts +2 -2
  361. package/src/state-adoption/linux-plan.ts +2 -2
  362. package/src/state-adoption/linux.ts +2 -2
  363. package/src/state-adoption/macos-plan.ts +2 -2
  364. package/src/state-adoption/macos.ts +1 -1
  365. package/src/steward.ts +7 -6
  366. package/src/task-dispatch-live.ts +1 -1
  367. package/src/task-dispatcher.ts +1 -1
  368. package/src/teams-config-store.ts +4 -3
  369. package/src/token-db-write.ts +10 -0
  370. package/src/workspace-command-settle.ts +2 -1
  371. package/src/workspace-steward-wake.ts +2 -2
  372. package/node_modules/@humwick/kernel/LICENSE +0 -167
  373. package/node_modules/@humwick/kernel/async-entity-persistence.ts +0 -344
  374. package/node_modules/@humwick/kernel/async-event-log.ts +0 -107
  375. package/node_modules/@humwick/kernel/automations.pilot.ts +0 -147
  376. package/node_modules/@humwick/kernel/config.ts +0 -111
  377. package/node_modules/@humwick/kernel/contract.ts +0 -572
  378. package/node_modules/@humwick/kernel/control-authz.ts +0 -281
  379. package/node_modules/@humwick/kernel/control.ts +0 -645
  380. package/node_modules/@humwick/kernel/effects.ts +0 -493
  381. package/node_modules/@humwick/kernel/event-log.ts +0 -319
  382. package/node_modules/@humwick/kernel/http-transport.ts +0 -384
  383. package/node_modules/@humwick/kernel/identity-grammar.ts +0 -20
  384. package/node_modules/@humwick/kernel/identity.ts +0 -248
  385. package/node_modules/@humwick/kernel/index.ts +0 -28
  386. package/node_modules/@humwick/kernel/kernel-persistence.ts +0 -181
  387. package/node_modules/@humwick/kernel/landing-incident.ts +0 -306
  388. package/node_modules/@humwick/kernel/landing.ts +0 -953
  389. package/node_modules/@humwick/kernel/manifest-validation.ts +0 -460
  390. package/node_modules/@humwick/kernel/mcp-transport.ts +0 -109
  391. package/node_modules/@humwick/kernel/message-authority-chaos-worker.ts +0 -200
  392. package/node_modules/@humwick/kernel/message-authority.ts +0 -281
  393. package/node_modules/@humwick/kernel/module-host.ts +0 -722
  394. package/node_modules/@humwick/kernel/openapi-init.ts +0 -14
  395. package/node_modules/@humwick/kernel/package.json +0 -47
  396. package/node_modules/@humwick/kernel/persistence-schema.ts +0 -248
  397. package/node_modules/@humwick/kernel/persistence.ts +0 -510
  398. package/node_modules/@humwick/kernel/registry.ts +0 -60
  399. package/node_modules/@humwick/kernel/sqlite-kernel-persistence.ts +0 -499
  400. package/node_modules/@humwick/kernel/sqlite-module-host.ts +0 -87
  401. package/node_modules/@humwick/kernel/timers-chaos-worker.ts +0 -186
  402. package/node_modules/@humwick/kernel/timers.pilot.ts +0 -1411
  403. package/node_modules/@humwick/kernel/workflow-activation.ts +0 -25
  404. package/node_modules/@humwick/kernel/workflow-backstop.ts +0 -40
  405. package/node_modules/@humwick/kernel/workflow-cancel-tree.ts +0 -20
  406. package/node_modules/@humwick/kernel/workflow-chaos-worker.ts +0 -213
  407. package/node_modules/@humwick/kernel/workflow-composition.ts +0 -231
  408. package/node_modules/@humwick/kernel/workflow-container.ts +0 -72
  409. package/node_modules/@humwick/kernel/workflow-control.test-fixture.ts +0 -42
  410. package/node_modules/@humwick/kernel/workflow-control.ts +0 -178
  411. package/node_modules/@humwick/kernel/workflow-definition.ts +0 -507
  412. package/node_modules/@humwick/kernel/workflow-dispatch.ts +0 -60
  413. package/node_modules/@humwick/kernel/workflow-foreach.ts +0 -167
  414. package/node_modules/@humwick/kernel/workflow-journal-codec.ts +0 -339
  415. package/node_modules/@humwick/kernel/workflow-journal.ts +0 -922
  416. package/node_modules/@humwick/kernel/workflow-lane.ts +0 -48
  417. package/node_modules/@humwick/kernel/workflow-lease-owner.ts +0 -60
  418. package/node_modules/@humwick/kernel/workflow-recovery.test-fixture.ts +0 -19
  419. package/node_modules/@humwick/kernel/workflow-runtime.ts +0 -938
  420. package/node_modules/@humwick/kernel/workflow-script.ts +0 -11
  421. package/node_modules/@humwick/kernel/workflow-state.ts +0 -299
  422. package/node_modules/@humwick/kernel/workflow-step-commit.ts +0 -195
  423. package/node_modules/@humwick/kernel/workflow-transitions.ts +0 -317
  424. package/node_modules/@humwick/kernel/workflow-values.ts +0 -153
  425. package/node_modules/@humwick/kernel/workflow.pilot.ts +0 -376
  426. package/node_modules/@humwick/offload/LICENSE +0 -167
  427. package/node_modules/@humwick/offload/README.md +0 -57
  428. package/node_modules/@humwick/offload/dist/async-runner.d.ts +0 -18
  429. package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +0 -1
  430. package/node_modules/@humwick/offload/dist/async-runner.js +0 -158
  431. package/node_modules/@humwick/offload/dist/async-runner.js.map +0 -1
  432. package/node_modules/@humwick/offload/dist/envelope.d.ts +0 -25
  433. package/node_modules/@humwick/offload/dist/envelope.d.ts.map +0 -1
  434. package/node_modules/@humwick/offload/dist/envelope.js +0 -80
  435. package/node_modules/@humwick/offload/dist/envelope.js.map +0 -1
  436. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +0 -10
  437. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +0 -1
  438. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +0 -59
  439. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +0 -1
  440. package/node_modules/@humwick/offload/dist/index.d.ts +0 -5
  441. package/node_modules/@humwick/offload/dist/index.d.ts.map +0 -1
  442. package/node_modules/@humwick/offload/dist/index.js +0 -5
  443. package/node_modules/@humwick/offload/dist/index.js.map +0 -1
  444. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +0 -62
  445. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +0 -1
  446. package/node_modules/@humwick/offload/dist/subprocess-runner.js +0 -295
  447. package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +0 -1
  448. package/node_modules/@humwick/offload/dist/types.d.ts +0 -111
  449. package/node_modules/@humwick/offload/dist/types.d.ts.map +0 -1
  450. package/node_modules/@humwick/offload/dist/types.js +0 -176
  451. package/node_modules/@humwick/offload/dist/types.js.map +0 -1
  452. package/node_modules/@humwick/offload/dist/worker-pool.d.ts +0 -23
  453. package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +0 -1
  454. package/node_modules/@humwick/offload/dist/worker-pool.js +0 -435
  455. package/node_modules/@humwick/offload/dist/worker-pool.js.map +0 -1
  456. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +0 -2
  457. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +0 -1
  458. package/node_modules/@humwick/offload/dist/worker-runtime.js +0 -27
  459. package/node_modules/@humwick/offload/dist/worker-runtime.js.map +0 -1
  460. package/node_modules/@humwick/offload/dist/worker.d.ts +0 -5
  461. package/node_modules/@humwick/offload/dist/worker.d.ts.map +0 -1
  462. package/node_modules/@humwick/offload/dist/worker.js +0 -1
  463. package/node_modules/@humwick/offload/dist/worker.js.map +0 -1
  464. package/node_modules/@humwick/offload/package.json +0 -46
  465. package/node_modules/@humwick/offload/src/async-runner.ts +0 -170
  466. package/node_modules/@humwick/offload/src/envelope.ts +0 -92
  467. package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +0 -49
  468. package/node_modules/@humwick/offload/src/index.ts +0 -4
  469. package/node_modules/@humwick/offload/src/subprocess-runner.ts +0 -334
  470. package/node_modules/@humwick/offload/src/types.ts +0 -235
  471. package/node_modules/@humwick/offload/src/worker-pool.ts +0 -463
  472. package/node_modules/@humwick/offload/src/worker-runtime.ts +0 -32
  473. package/node_modules/@humwick/offload/src/worker.ts +0 -5
  474. package/public/assets/agent-liveness-DyqxfB-t.js +0 -2
  475. package/public/assets/agent-profiles-BpI_jTxq.js +0 -2
  476. package/public/assets/fleet-BL4UXS_k.js +0 -2
  477. package/public/assets/index-B7ZerHLY.css +0 -2
  478. package/public/assets/index-BK0WV6hF.js +0 -15
  479. package/public/assets/projects-B_lmcqfO.js +0 -2
  480. package/public/assets/provider-display-DbLWpP3j.js +0 -2
  481. package/public/assets/run-card-CO_bguQf.js +0 -2
  482. package/public/assets/use-project-list-B-T2Bnnn.js +0 -2
  483. package/public/assets/versioned-prompt-editor-Diu8BIaM.js +0 -8
  484. package/runner/src/gate-run-lease-probe.ts +0 -38
  485. package/src/db/claude-catalog-reconcile.ts +0 -27
  486. package/src/routing-model-aliases.ts +0 -21
@@ -19,6 +19,8 @@ import { applyIdleSelfCare } from "./idle-self-care";
19
19
  // write (none of these are) would need `idempotent: false` plus a caller-supplied key.
20
20
 
21
21
  import {
22
+ applyAgentLifecycleUpdateInternal,
23
+ type AgentLifecycleWriteArgs,
22
24
  clearAgentMetaKeys,
23
25
  deleteAgentInternal,
24
26
  heartbeat,
@@ -41,6 +43,7 @@ import {
41
43
  import { pruneOfflineAgentsWithFinality, type AgentsPruneOfflineOpArgs } from "./agents-prune-write-op.ts";
42
44
  import { setAgentKeepState } from "./agent-keep.ts";
43
45
  import type { AgentCard, AgentKeep, AgentSessionGuard, ContextState, ManagedSessionExitDiagnostics, ProviderCapabilities, QuotaState, RegisterAgentInput } from "../types";
46
+ import type { OrphanedTokenPage } from "./agents.ts";
44
47
  import type { LineageRevokeReason } from "./token-standing.ts";
45
48
  import type { LifecycleWriteOutcome } from "./lifecycle-seq.ts";
46
49
  import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
@@ -69,31 +72,36 @@ export interface AgentsHeartbeatOpArgs {
69
72
  export interface AgentsRecordContextSnapshotOpArgs { agentId: string; context: ContextState }
70
73
  export interface AgentsReapStaleOpArgs { ttlMs: number; lagGraceMs: number; protectedIds?: string[]; seq: number }
71
74
  export interface AgentsDeleteOpArgs { id: string }
72
- export interface AgentsRevokeOrphanedTokensOpArgs { graceMs: number; lagGraceMs: number }
75
+ export interface AgentsRevokeOrphanedTokensOpArgs { graceMs: number; lagGraceMs: number; afterId?: string; limit?: number }
73
76
  export interface AgentsRevokeTokensForAgentOpArgs { agentId: string; reason?: LineageRevokeReason }
74
77
 
75
78
  export const AGENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
76
- "agents.pruneIdentityArchive": { idempotent: true, fn: (_args: Record<string, never>, ctx: WriterOpContext) => pruneAgentIdentityArchive(ctx.now) },
77
- "agents.idleSelfCare": { idempotent: false, fn: (args: { id: string; epoch: number }, ctx: WriterOpContext) => applyIdleSelfCare(args, ctx.now) },
79
+ "agents.pruneIdentityArchive": { offLoop: true, idempotent: true, fn: (_args: Record<string, never>, ctx: WriterOpContext) => pruneAgentIdentityArchive(ctx.now) },
80
+ "agents.idleSelfCare": { offLoop: true, idempotent: false, fn: (args: { id: string; epoch: number }, ctx: WriterOpContext) => applyIdleSelfCare(args, ctx.now) },
78
81
  "agents.upsert": {
82
+ offLoop: true,
79
83
  idempotent: true,
80
84
  fn: (args: AgentsUpsertOpArgs, ctx: WriterOpContext): UpsertAgentTxResult =>
81
85
  upsertAgentInternal(args.input, args.opts, ctx.now, args.seq),
82
86
  },
83
87
  "agents.setLabel": {
88
+ offLoop: true,
84
89
  idempotent: true,
85
90
  fn: (args: AgentsSetLabelOpArgs): boolean => setLabel(args.id, args.label),
86
91
  },
87
92
  "agents.setKeep": {
93
+ offLoop: true,
88
94
  idempotent: true,
89
95
  fn: (args: { id: string; keep: AgentKeep | null }): boolean => setAgentKeepState(args.id, args.keep),
90
96
  },
91
97
  "agents.setTeam": {
98
+ offLoop: true,
92
99
  idempotent: true,
93
100
  fn: (args: AgentsSetTeamOpArgs, ctx: WriterOpContext): AgentCard | null =>
94
101
  setAgentTeam(args.agentId, args.teamId, ctx.now),
95
102
  },
96
103
  "agents.setTags": {
104
+ offLoop: true,
97
105
  idempotent: true,
98
106
  fn: (args: AgentsSetTagsOpArgs, ctx: WriterOpContext): AgentCard | null =>
99
107
  setTags(args.id, args.tags, ctx.now),
@@ -102,6 +110,12 @@ export const AGENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.f
102
110
  // coalescing window buffered them) and return a tri-state: `superseded` means a later-submitted
103
111
  // lifecycle write already landed, which is a SUCCESS for the caller and must not be mistaken for a
104
112
  // missing agent. See lifecycle-seq.ts.
113
+ "agents.applyLifecycleUpdate": {
114
+ // #1870 S7: bus status frame and heartbeat bypass; a replay of the same `seq` is a no-op on the fenced columns.
115
+ offLoop: true,
116
+ idempotent: true,
117
+ fn: (args: AgentLifecycleWriteArgs) => applyAgentLifecycleUpdateInternal(args),
118
+ },
105
119
  "agents.setStatus": {
106
120
  offLoop: true,
107
121
  idempotent: true,
@@ -115,16 +129,19 @@ export const AGENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.f
115
129
  markReady(args.id, args.ready, args.guard, ctx.now, args.seq),
116
130
  },
117
131
  "agents.mergeMeta": {
132
+ offLoop: true,
118
133
  idempotent: true,
119
134
  fn: (args: AgentsMergeMetaOpArgs, ctx: WriterOpContext): boolean =>
120
135
  mergeAgentMeta(args.id, args.meta, args.guard, ctx.now),
121
136
  },
122
137
  "agents.markReadyNotified": {
138
+ offLoop: true,
123
139
  idempotent: true,
124
140
  fn: (args: AgentsMarkReadyNotifiedOpArgs, ctx: WriterOpContext): boolean =>
125
141
  markAgentReadyNotified(args.id, ctx.now),
126
142
  },
127
143
  "agents.clearMetaKeys": {
144
+ offLoop: true,
128
145
  idempotent: true,
129
146
  fn: (args: AgentsClearMetaKeysOpArgs): boolean => clearAgentMetaKeys(args.id, args.keys),
130
147
  },
@@ -135,11 +152,13 @@ export const AGENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.f
135
152
  recordAgentExitDiagnosticsInternal(args.id, args.diagnostics, ctx.now, args.seq),
136
153
  },
137
154
  "agents.heartbeat": {
155
+ offLoop: true,
138
156
  idempotent: true,
139
157
  fn: (args: AgentsHeartbeatOpArgs, ctx: WriterOpContext): boolean =>
140
158
  heartbeat(args.id, args.guard, args.runtime, ctx.now),
141
159
  },
142
160
  "agents.recordContextSnapshot": {
161
+ offLoop: true,
143
162
  idempotent: true,
144
163
  fn: (args: AgentsRecordContextSnapshotOpArgs, ctx: WriterOpContext): null => {
145
164
  recordContextSnapshot(args.agentId, args.context, ctx.now);
@@ -147,26 +166,30 @@ export const AGENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.f
147
166
  },
148
167
  },
149
168
  "agents.reapStale": {
169
+ offLoop: true,
150
170
  idempotent: true,
151
171
  fn: (args: AgentsReapStaleOpArgs, ctx: WriterOpContext): string[] =>
152
172
  reapStaleAgentsInternal(args.ttlMs, args.lagGraceMs, ctx.now, args.protectedIds, args.seq),
153
173
  },
154
174
  "agents.pruneOffline": {
175
+ offLoop: true,
155
176
  idempotent: true,
156
177
  fn: (args: AgentsPruneOfflineOpArgs, ctx: WriterOpContext) =>
157
178
  pruneOfflineAgentsWithFinality(args, ctx.now),
158
179
  },
159
180
  "agents.delete": {
181
+ offLoop: true,
160
182
  idempotent: true,
161
183
  fn: (args: AgentsDeleteOpArgs, ctx: WriterOpContext): boolean => deleteAgentInternal(args.id, ctx.now),
162
184
  },
163
185
  "agents.revokeOrphanedTokens": {
164
186
  offLoop: true,
165
187
  idempotent: true,
166
- fn: (args: AgentsRevokeOrphanedTokensOpArgs, ctx: WriterOpContext): Array<{ agentId: string; jtis: string[] }> =>
167
- revokeOrphanedRuntimeTokens(args.graceMs, args.lagGraceMs, ctx.now),
188
+ fn: (args: AgentsRevokeOrphanedTokensOpArgs, ctx: WriterOpContext): OrphanedTokenPage =>
189
+ revokeOrphanedRuntimeTokens(args.graceMs, args.lagGraceMs, ctx.now, { afterId: args.afterId, limit: args.limit }),
168
190
  },
169
191
  "agents.revokeTokensForAgent": {
192
+ offLoop: true,
170
193
  idempotent: true,
171
194
  fn: (args: AgentsRevokeTokensForAgentOpArgs, ctx: WriterOpContext): string[] =>
172
195
  revokeRuntimeTokensForAgent(args.agentId, ctx.now, args.reason),
package/src/db/agents.ts CHANGED
@@ -479,28 +479,51 @@ export function applyAgentLifecycleUpdate(
479
479
  guard?: AgentSessionGuard,
480
480
  now: number = Date.now(),
481
481
  ): AgentLifecycleUpdateResult {
482
- const miss: AgentLifecycleUpdateResult = { ok: false, changed: false, metaWriteBytes: 0, before: null, after: null };
483
- const before = getPersistedAgentRuntimeCard(id);
484
- if (!before) return miss;
485
- if (!agentSessionGuardResult(before, guard).ok) return miss;
482
+ const seq = nextLifecycleSeq();
483
+ const before = prepareAgentLifecycleUpdate(id, update, guard);
484
+ if (!before) return { ok: false, changed: false, metaWriteBytes: 0, before: null, after: null };
485
+ return { ok: true, before, ...inWriteTransaction(() => applyAgentLifecycleUpdateInternal(agentLifecycleWriteArgs(before, update, seq, now))) };
486
+ }
486
487
 
488
+ /** The writer-op args for one lifecycle delta (#1870 S7). `seq` is taken at the caller's entry (lifecycle-seq.ts
489
+ * rule 1); `expectedEpoch` fences the write to the instance `before` was read from across the writer hop. */
490
+ export interface AgentLifecycleWriteArgs {
491
+ id: string;
492
+ update: AgentLifecycleUpdate;
493
+ seq: number;
494
+ now: number;
495
+ expectedEpoch: number;
496
+ before: { instanceId?: string; spawnRequestId: string; terminalProviderExit?: unknown };
497
+ }
498
+
499
+ export function agentLifecycleWriteArgs(before: AgentCard, update: AgentLifecycleUpdate, seq: number, now: number): AgentLifecycleWriteArgs {
500
+ return { id: before.id, update, seq, now, expectedEpoch: before.epoch, before: { instanceId: before.instanceId, spawnRequestId: String(before.meta?.spawnRequestId ?? ""), terminalProviderExit: before.meta?.terminalProviderExit } };
501
+ }
502
+
503
+ /** The main-thread half of a lifecycle delta: guard, `before` snapshot, transition handlers and presence
504
+ * eviction (main-thread state), all before the write is submitted. Null when the guard rejects. */
505
+ export function prepareAgentLifecycleUpdate(id: string, update: AgentLifecycleUpdate, guard?: AgentSessionGuard): AgentCard | null {
506
+ const before = getPersistedAgentRuntimeCard(id);
507
+ if (!before || !agentSessionGuardResult(before, guard).ok) return null;
487
508
  const nextStatus = update.status;
488
- // Pre-transition handlers are main-thread-only state (see runBeforeAgentStatusTransitionHandlers)
489
- // and must run BEFORE the write lands, exactly as `setStatus` does.
509
+ // Pre-transition handlers must run BEFORE the write lands, exactly as `setStatus` does.
490
510
  if (nextStatus !== undefined && nextStatus !== before.status) runBeforeAgentStatusTransitionHandlers(before, nextStatus);
491
511
  // #1468 — a terminal transition evicts any buffered beat so a coalesced advance cannot later
492
512
  // resurrect a gone agent at flush.
493
513
  if (nextStatus === "offline" || nextStatus === "stale") evictPendingPresence(id);
514
+ return before;
515
+ }
494
516
 
495
- // #2111 review BLOCKER 1 — raise each written column's ordinal so a lifecycle write the coalescing
496
- // window deferred cannot commit afterwards and undo it. This path only ever WINS its own races (it
497
- // commits synchronously, and any submission carrying a higher ordinal has not been allocated yet),
498
- // but it is fenced anyway so every writer of these columns obeys one rule.
499
- const seq = nextLifecycleSeq();
517
+ /** Op body (`agents.applyLifecycleUpdate`): the fenced UPDATE, its effects, and the offline post-commit, in the
518
+ * caller's transaction. #2111 review BLOCKER 1 — each written column's ordinal is raised, so a lifecycle write
519
+ * the coalescing window deferred cannot commit afterwards and undo it. */
520
+ export function applyAgentLifecycleUpdateInternal(args: AgentLifecycleWriteArgs): Omit<AgentLifecycleUpdateResult, "ok" | "before"> {
521
+ const { id, update, seq, now, before } = args;
522
+ const nextStatus = update.status;
500
523
  // Named params throughout: the fenced-column fragments repeat `$seq`, which positional `?` could
501
524
  // only express by counting placeholders across a dynamically-built SET list.
502
525
  const sets: string[] = ["last_seen = $now"];
503
- const params: Record<string, unknown> = { $now: now, $seq: seq, $id: id };
526
+ const params: Record<string, unknown> = { $now: now, $seq: seq, $id: id, $epoch: args.expectedEpoch };
504
527
  let metaWriteBytes = 0;
505
528
  if (nextStatus !== undefined) {
506
529
  sets.push(fencedColumnSql("status", "$status", "$seq"));
@@ -558,16 +581,10 @@ export function applyAgentLifecycleUpdate(
558
581
  params.$qs = runtime?.quota ? JSON.stringify(runtime.quota) : null;
559
582
  }
560
583
 
561
- // BEGIN IMMEDIATE (#1428/#1431) so busy_timeout governs contention, joining an ambient writer-op
562
- // transaction rather than nesting when this body is reached through the gateway.
563
- const changed = inWriteTransaction(() => {
564
- const applied = getDb().query(`UPDATE agents SET ${sets.join(", ")} WHERE id = $id`).run(params as never).changes > 0;
565
- const terminalLaunch = terminalProviderLaunchId(update.meta?.terminalProviderExit ?? before.meta?.terminalProviderExit);
566
- if (applied && nextStatus === "offline" && terminalLaunch) revokeVaultTokens(id, now, "provider_terminal_exit", before.instanceId, String(before.meta?.spawnRequestId ?? ""), terminalLaunch);
567
- if (applied && contextState) recordContextSnapshot(id, contextState, now);
568
- return applied;
569
- });
570
-
584
+ const changed = getDb().query(`UPDATE agents SET ${sets.join(", ")} WHERE id = $id AND epoch = $epoch`).run(params as never).changes > 0;
585
+ const terminalLaunch = terminalProviderLaunchId(update.meta?.terminalProviderExit ?? before.terminalProviderExit);
586
+ if (changed && nextStatus === "offline" && terminalLaunch) revokeVaultTokens(id, now, "provider_terminal_exit", before.instanceId, before.spawnRequestId, terminalLaunch);
587
+ if (changed && contextState) recordContextSnapshot(id, contextState, now);
571
588
  // NOTE (from setStatus): `offline` is a LIVENESS observation, not a confirmed death — it re-elects
572
589
  // stewards and closes pairs but must NOT release a steward's workspace claim (#1789 part 2, F2).
573
590
  // Gated on the requested status, not on a status CHANGE, matching setStatus's own condition.
@@ -575,7 +592,7 @@ export function applyAgentLifecycleUpdate(
575
592
  closeOpenPairsForAgent(id, now);
576
593
  electWorkspaceStewards();
577
594
  }
578
- return { ok: true, changed, metaWriteBytes, before, after: getPersistedAgentRuntimeCard(id) };
595
+ return { changed, metaWriteBytes, after: getPersistedAgentRuntimeCard(id) };
579
596
  }
580
597
 
581
598
  export function mergeAgentMeta(id: string, meta: Record<string, unknown>, guard?: AgentSessionGuard, now: number = Date.now()): boolean {
@@ -828,21 +845,27 @@ export function heartbeat(
828
845
  // withBufferedPresence / bufferedLiveAgentIds) live in the leaf module ./presence-buffer.ts so every
829
846
  // liveness reader can fold in the buffer without an import cycle (agents.ts imports channels.ts).
830
847
 
848
+ /** Heartbeat: buffers the steady-state liveness advance (flushed by `flushPresence`), writes presence-critical
849
+ * transitions at once. False if the session guard rejects or the agent is gone. Sync twin for tests; the
850
+ * transports use `recordHeartbeatAsync`, which writes the bypass on the writer thread (#1870 S7). */
851
+ export function recordHeartbeat(id: string, opts: HeartbeatInput = {}): boolean {
852
+ const beat = bufferOrBypassHeartbeat(id, opts);
853
+ return typeof beat === "boolean" ? beat : applyAgentLifecycleUpdate(id, beat, opts.guard, opts.now ?? Date.now()).changed;
854
+ }
855
+
856
+ export interface HeartbeatInput {
857
+ guard?: AgentSessionGuard;
858
+ /** Semantic status reported by the beat (bus path); undefined for the runtime-less API path. */
859
+ status?: AgentCard["status"];
860
+ runtime?: { providerCapabilities?: ProviderCapabilities; context?: ContextState; quota?: QuotaState };
861
+ now?: number;
862
+ }
863
+
831
864
  /**
832
- * Hot-path heartbeat entry point for the bus/API transports. Coalesces the steady-state liveness
833
- * advance into the in-memory buffer (flushed by `flushPresence`), but writes synchronously for
834
- * presence-critical transitions. Returns false if the session guard rejects or the agent is gone.
865
+ * The shared decision of `recordHeartbeat` and `recordHeartbeatAsync`: buffer a steady-state beat (returns
866
+ * the result) or hand back the lifecycle delta a presence-critical beat must write now.
835
867
  */
836
- export function recordHeartbeat(
837
- id: string,
838
- opts: {
839
- guard?: AgentSessionGuard;
840
- /** Semantic status reported by the beat (bus path); undefined for the runtime-less API path. */
841
- status?: AgentCard["status"];
842
- runtime?: { providerCapabilities?: ProviderCapabilities; context?: ContextState; quota?: QuotaState };
843
- now?: number;
844
- } = {},
845
- ): boolean {
868
+ export function bufferOrBypassHeartbeat(id: string, opts: HeartbeatInput): boolean | AgentLifecycleUpdate {
846
869
  if (!validateAgentSession(id, opts.guard).ok) return false;
847
870
  const current = getAgent(id);
848
871
  if (!current) return false;
@@ -861,9 +884,9 @@ export function recordHeartbeat(
861
884
  opts.runtime.context.lifecycleState !== current.context?.lifecycleState;
862
885
  const prev = pendingPresence.get(id);
863
886
  if (resurrection || statusChanged || lifecycleTransition) {
864
- // Merge any buffered-but-unflushed runtime cols into this synchronous write so the bypass never
865
- // DISCARDS a coalesced advance (a beat that carried context but was buffered, then a status
866
- // change arrives carrying none). Per column: this beat wins, else the pending value.
887
+ // Merge any buffered-but-unflushed runtime cols into this write so the bypass never DISCARDS a
888
+ // coalesced advance (a beat that carried context but was buffered, then a status change arrives
889
+ // carrying none). Per column: this beat wins, else the pending value.
867
890
  const runtime = prev
868
891
  ? {
869
892
  providerCapabilities: opts.runtime?.providerCapabilities ?? prev.providerCapabilities,
@@ -871,16 +894,10 @@ export function recordHeartbeat(
871
894
  quota: opts.runtime?.quota ?? prev.quota,
872
895
  }
873
896
  : opts.runtime;
874
- pendingPresence.delete(id); // this synchronous write supersedes any buffered advance
875
- // #2111 — was `heartbeat()` (one UPDATE) followed by `setStatus()` (a second UPDATE of the SAME
876
- // row inside its own BEGIN IMMEDIATE). One consolidated update now carries both; the explicit
877
- // status overrides the resurrection CASE exactly as the second write used to.
878
- return applyAgentLifecycleUpdate(
879
- id,
880
- { status: statusChanged ? opts.status : undefined, resurrectOnBeat: true, runtime },
881
- opts.guard,
882
- now,
883
- ).changed;
897
+ pendingPresence.delete(id); // this write supersedes any buffered advance
898
+ // #2111 — one consolidated update carries both the beat and the explicit status, which overrides
899
+ // the resurrection CASE.
900
+ return { status: statusChanged ? opts.status : undefined, resurrectOnBeat: true, runtime };
884
901
  }
885
902
 
886
903
  // Steady state: buffer. Merge context advisory markers now (matches heartbeat()'s per-beat merge),
@@ -1192,6 +1209,10 @@ export function deleteAgent(id: string): { ok: boolean; error?: string } {
1192
1209
  return deleted ? { ok: true } : { ok: false, error: "agent not found" };
1193
1210
  }
1194
1211
 
1212
+ export type OrphanedTokenRevocation = { agentId: string; jtis: string[] };
1213
+ /** One page of the orphan sweep: agents after `afterId` in id order, up to `limit` (#3776: one short write each). */
1214
+ export type OrphanedTokenPage = { revoked: OrphanedTokenRevocation[]; nextAfterId: string | null };
1215
+
1195
1216
  // #1057(1) — close the orphan-token window #1051 left open. A dead runner with no replacement
1196
1217
  // used to keep its token valid until the 24h offline prune (~720× the pre-#1051 ~2min horizon).
1197
1218
  // This sweep revokes ("stale", re-mintable — #484) the runtime tokens of agents whose process the
@@ -1204,12 +1225,18 @@ export function deleteAgent(id: string): { ok: boolean; error?: string } {
1204
1225
  // `lagGraceMs` mirrors the stale reaper's #1048/#1055 discipline: while the relay's own event
1205
1226
  // loop was stalled, a live claimant's heartbeats may not have been processed yet, so the window
1206
1227
  // widens by the observed lag rather than misreading the backlog as post-mortem silence.
1207
- export function revokeOrphanedRuntimeTokens(graceMs: number = STALE_TTL_MS, lagGraceMs = 0, now = Date.now()): Array<{ agentId: string; jtis: string[] }> {
1228
+ export function revokeOrphanedRuntimeTokens(
1229
+ graceMs: number = STALE_TTL_MS,
1230
+ lagGraceMs = 0,
1231
+ now = Date.now(),
1232
+ page: { afterId?: string; limit?: number } = {},
1233
+ ): OrphanedTokenPage {
1208
1234
  const cutoff = now - graceMs - Math.max(0, lagGraceMs);
1235
+ const limit = page.limit ?? -1;
1209
1236
  const rows = getDb()
1210
- .query(`SELECT id, meta, last_seen FROM agents WHERE status = 'offline' AND meta LIKE '%lastExitReceivedAt%' AND NOT ${reservedAgentIdSql()}`)
1211
- .all() as Array<{ id: string; meta: string | null; last_seen: number }>;
1212
- const revoked: Array<{ agentId: string; jtis: string[] }> = [];
1237
+ .query(`SELECT id, meta, last_seen FROM agents WHERE status = 'offline' AND meta LIKE '%lastExitReceivedAt%' AND NOT ${reservedAgentIdSql()} AND id > ? ORDER BY id LIMIT ?`)
1238
+ .all(page.afterId ?? "", limit) as Array<{ id: string; meta: string | null; last_seen: number }>;
1239
+ const revoked: OrphanedTokenRevocation[] = [];
1213
1240
  for (const row of rows) {
1214
1241
  const meta = parseJson<Record<string, unknown>>(row.meta ?? "{}", {});
1215
1242
  const receivedAt = typeof meta.lastExitReceivedAt === "number" ? meta.lastExitReceivedAt : undefined;
@@ -1218,7 +1245,7 @@ export function revokeOrphanedRuntimeTokens(graceMs: number = STALE_TTL_MS, lagG
1218
1245
  const jtis = revokeProviderRuntimeTokensByJti(runtimeTokenJtisFromMeta(meta), "stale", now);
1219
1246
  if (jtis.length > 0) revoked.push({ agentId: row.id, jtis });
1220
1247
  }
1221
- return revoked;
1248
+ return { revoked, nextAfterId: limit > 0 && rows.length === limit ? rows[rows.length - 1]!.id : null };
1222
1249
  }
1223
1250
 
1224
1251
  // #484: `reason` is stamped on each revocation so a stale-on-restart revocation
@@ -13,6 +13,7 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
13
13
 
14
14
  export const ARTIFACTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
15
15
  "artifacts.upsertBlob": {
16
+ offLoop: true,
16
17
  idempotent: true,
17
18
  fn: (args: { input: Parameters<typeof upsertArtifactBlob>[0] }, ctx: WriterOpContext) =>
18
19
  upsertArtifactBlob({ ...args.input, createdAt: args.input.createdAt ?? ctx.now }),
@@ -22,23 +23,27 @@ export const ARTIFACTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Objec
22
23
  idempotent: false,
23
24
  fn: (args: { input: Parameters<typeof createArtifact>[0] }, ctx: WriterOpContext) => createArtifact(args.input, ctx.now),
24
25
  },
25
- "artifacts.pin": { idempotent: true, fn: (args: { id: string; pinned: boolean; readScope?: import("./artifacts").ArtifactReadScope }) => setArtifactPinned(args.id, args.pinned, args.readScope) },
26
- "artifacts.delete": { idempotent: true, fn: (args: { id: string }) => deleteArtifact(args.id, false) },
27
- "artifacts.deleteBlob": { idempotent: true, fn: (args: { digest: string }) => deleteArtifactBlob(args.digest) },
26
+ "artifacts.pin": { offLoop: true, idempotent: true, fn: (args: { id: string; pinned: boolean; readScope?: import("./artifacts").ArtifactReadScope }) => setArtifactPinned(args.id, args.pinned, args.readScope) },
27
+ "artifacts.delete": { offLoop: true, idempotent: true, fn: (args: { id: string }) => deleteArtifact(args.id, false) },
28
+ "artifacts.deleteBlob": { offLoop: true, idempotent: true, fn: (args: { digest: string }) => deleteArtifactBlob(args.digest) },
28
29
  "artifacts.link": {
30
+ offLoop: true,
29
31
  idempotent: true,
30
32
  fn: (args: { input: Parameters<typeof linkArtifact>[0] }, ctx: WriterOpContext) => linkArtifact(args.input, ctx.now),
31
33
  },
32
34
  "artifacts.linkExistingEntity": {
35
+ offLoop: true,
33
36
  idempotent: true,
34
37
  fn: (args: { input: Parameters<typeof linkArtifactToExistingEntity>[0] }, ctx: WriterOpContext) => linkArtifactToExistingEntity(args.input, ctx.now),
35
38
  },
36
39
  "artifacts.deleteLinksForEntity": {
40
+ offLoop: true,
37
41
  idempotent: true,
38
42
  fn: (args: { entityType: Parameters<typeof deleteArtifactLinksForEntity>[0]; entityId: string | number }) =>
39
43
  deleteArtifactLinksForEntity(args.entityType, args.entityId),
40
44
  },
41
45
  "artifacts.sweep": {
46
+ offLoop: true,
42
47
  idempotent: true,
43
48
  fn: (args: { input?: Parameters<typeof sweepArtifacts>[0] }, ctx: WriterOpContext) =>
44
49
  sweepArtifacts({ ...args.input, now: args.input?.now ?? ctx.now }, false),
@@ -49,6 +49,7 @@ export const BUS_OUTBOX_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Obje
49
49
  // the same end state (no rows older than the threshold), so it is idempotent and needs no key. `ctx.now`
50
50
  // is the submission-time wall-clock (H6).
51
51
  "bus.prune": {
52
+ offLoop: true,
52
53
  idempotent: true,
53
54
  fn: (args: BusPruneOpArgs, ctx: WriterOpContext): { changes: number } => ({
54
55
  changes: pruneOutbox(args.retentionMs, ctx.now),
@@ -14,15 +14,18 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
14
14
 
15
15
  export const CHANNELS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
16
16
  "channels.deleteBinding": {
17
+ offLoop: true,
17
18
  idempotent: true,
18
19
  fn: (args: { id: string; fence: ChannelBindingSessionFence }) => deleteChannelBindingBody(args.id, args.fence),
19
20
  },
20
21
  "channels.upsertBinding": {
22
+ offLoop: true,
21
23
  idempotent: true,
22
24
  fn: (args: { input: UpsertChannelBindingInput; presence?: PresenceSnapshot; fence?: ChannelBindingSessionFence }, ctx: WriterOpContext) =>
23
25
  upsertChannelBindingBody(args.input, ctx.now, args.presence, args.fence),
24
26
  },
25
27
  "channels.registerAgent": {
28
+ offLoop: true,
26
29
  idempotent: true,
27
30
  fn: (args: { agentId: string; expectedEpoch: number; presence?: PresenceSnapshot }, ctx: WriterOpContext) => {
28
31
  // Re-read under this operation's BEGIN IMMEDIATE. A card captured before an await may now be
@@ -36,6 +39,7 @@ export const CHANNELS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
36
39
  },
37
40
  },
38
41
  "channels.evaluatePoolBindings": {
42
+ offLoop: true,
39
43
  idempotent: true,
40
44
  fn: (args: { presence?: PresenceSnapshot }, ctx: WriterOpContext) => evaluatePoolBindingsBody(ctx.now, args.presence),
41
45
  },
@@ -35,15 +35,15 @@ const write = <T>(op: string, args: unknown, idempotencyKey?: string): Promise<T
35
35
  export function upsertWatchStateAsync(input: Parameters<typeof upsertWatchState>[0]): Promise<ReturnType<typeof upsertWatchState>> {
36
36
  return write("ciIncidents.watch.upsert", input);
37
37
  }
38
- /** One gateway writer transaction for the complete watch + incident lifecycle decision. */
39
- export function ingestCiObservationTransitionAsync(input: CiObservationTransitionInput): Promise<CiObservationTransitionResult> {
40
- return write("ciIncidents.observation.ingest", input);
38
+ /** One gateway writer transaction for the complete watch + incident lifecycle decision. `now` pins a test clock. */
39
+ export function ingestCiObservationTransitionAsync(input: CiObservationTransitionInput, now?: number): Promise<CiObservationTransitionResult> {
40
+ return write("ciIncidents.observation.ingest", now === undefined ? input : { ...input, now });
41
41
  }
42
42
  export function markCiWatchNeverMatchedNotifiedAsync(watchKey: string): Promise<ReturnType<typeof markCiWatchNeverMatchedNotified>> {
43
43
  return write("ciIncidents.watch.markNeverMatched", { watchKey });
44
44
  }
45
- export function markCiRootMissingNotifiedAsync(repository: string): Promise<ReturnType<typeof markCiRootMissingNotified>> {
46
- return write("ciIncidents.root.markMissing", { repository });
45
+ export function markCiRootMissingNotifiedAsync(repository: string, now?: number): Promise<ReturnType<typeof markCiRootMissingNotified>> {
46
+ return write("ciIncidents.root.markMissing", now === undefined ? { repository } : { repository, now });
47
47
  }
48
48
  export function openOrDedupeIncidentAsync(input: Parameters<typeof openOrDedupeIncident>[0]): Promise<ReturnType<typeof openOrDedupeIncident>> {
49
49
  return write("ciIncidents.openOrDedupe", input);
@@ -27,7 +27,7 @@ export const CI_INCIDENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Ob
27
27
  "ciIncidents.observation.ingest": {
28
28
  offLoop: true,
29
29
  idempotent: true,
30
- fn: (a: Parameters<typeof applyCiObservationTransitionInternal>[0], ctx) => applyCiObservationTransitionInternal(a, ctx.now),
30
+ fn: (a: Parameters<typeof applyCiObservationTransitionInternal>[0] & { now?: number }, ctx) => applyCiObservationTransitionInternal(a, a.now ?? ctx.now),
31
31
  },
32
32
  "ciIncidents.gateRun.record": { offLoop: true, idempotent: true, fn: (a: Parameters<typeof recordCiGateRuns>[0]) => recordCiGateRuns(a) },
33
33
  "ciIncidents.gateBackfill.start": { offLoop: true, idempotent: true, fn: (a: Parameters<typeof startCiGateBackfill>[0]) => startCiGateBackfill(a) },
@@ -36,7 +36,7 @@ export const CI_INCIDENTS_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Ob
36
36
  "ciIncidents.sha.prune": { offLoop: true, idempotent: true, fn: (_a: { now: number }, ctx) => pruneCiShaObservations(ctx.now) },
37
37
  "ciIncidents.watch.upsert": { offLoop: true, idempotent: true, fn: (a: Parameters<typeof upsertWatchState>[0], ctx) => upsertWatchState(a, ctx.now) },
38
38
  "ciIncidents.watch.markNeverMatched": { offLoop: true, idempotent: true, fn: (a: { watchKey: string }, ctx) => markCiWatchNeverMatchedNotified(a.watchKey, ctx.now) },
39
- "ciIncidents.root.markMissing": { offLoop: true, idempotent: true, fn: (a: { repository: string }, ctx) => markCiRootMissingNotified(a.repository, ctx.now) },
39
+ "ciIncidents.root.markMissing": { offLoop: true, idempotent: true, fn: (a: { repository: string; now?: number }, ctx) => markCiRootMissingNotified(a.repository, a.now ?? ctx.now) },
40
40
  "ciIncidents.openOrDedupe": { offLoop: true, idempotent: true, fn: (a: Parameters<typeof openOrDedupeIncidentInternal>[0], ctx) => openOrDedupeIncidentInternal(a, ctx.now, false) },
41
41
  // Guarded transitions throw after they have advanced; use the durable retry ledger rather than
42
42
  // misclassifying that as safe replay. Distinct steward attempts carry distinct request ids.
@@ -7,9 +7,9 @@ export const CLAUDE_API_BUDGET_READ_OPS = {
7
7
  "apiBudget.status": (args: { now: number; budgetId?: ApiBudgetId }) => apiBudgetStatus(args.now, args.budgetId),
8
8
  };
9
9
  export const CLAUDE_API_BUDGET_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = {
10
- "apiBudget.reconcile": { idempotent: true, fn: (args: { now: number }) => reconcileApiBudgetSessions(args.now) },
11
- "apiBudget.pace": { idempotent: true, fn: (args: { leadTimeMs: number; now: number; budgetId?: ApiBudgetId }) => apiBudgetPace(args.leadTimeMs, args.now, args.budgetId) },
12
- "apiBudget.configure": { idempotent: true, fn: (args: { input: ApiBudgetConfig; now: number }) => configureApiBudget(args.input, args.now) },
13
- "apiBudget.reserve": { idempotent: true, fn: (args: { input: ApiBudgetReserve; now: number }) => reserveApiBudget(args.input, args.now) },
14
- "apiBudget.report": { idempotent: true, fn: (args: { input: ApiBudgetReport; now: number }) => reportApiBudget(args.input, args.now) },
10
+ "apiBudget.reconcile": { offLoop: true, idempotent: true, fn: (args: { now: number }) => reconcileApiBudgetSessions(args.now) },
11
+ "apiBudget.pace": { offLoop: true, idempotent: true, fn: (args: { leadTimeMs: number; now: number; budgetId?: ApiBudgetId }) => apiBudgetPace(args.leadTimeMs, args.now, args.budgetId) },
12
+ "apiBudget.configure": { offLoop: true, idempotent: true, fn: (args: { input: ApiBudgetConfig; now: number }) => configureApiBudget(args.input, args.now) },
13
+ "apiBudget.reserve": { offLoop: true, idempotent: true, fn: (args: { input: ApiBudgetReserve; now: number }) => reserveApiBudget(args.input, args.now) },
14
+ "apiBudget.report": { offLoop: true, idempotent: true, fn: (args: { input: ApiBudgetReport; now: number }) => reportApiBudget(args.input, args.now) },
15
15
  };
@@ -44,13 +44,13 @@ function put(args: Put): boolean {
44
44
 
45
45
  export const CLAUDE_APP_OAUTH_READ_OPS = { "claudeAppOAuth.get": get };
46
46
  export const CLAUDE_APP_OAUTH_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = {
47
- "claudeAppOAuth.put": { idempotent: false, fn: put },
48
- "claudeAppOAuth.take": { idempotent: false, fn: (args: Key) => {
47
+ "claudeAppOAuth.put": { offLoop: true, idempotent: false, fn: put },
48
+ "claudeAppOAuth.take": { offLoop: true, idempotent: false, fn: (args: Key) => {
49
49
  const value = get(args);
50
50
  getDb().query("DELETE FROM claude_app_oauth WHERE resource=? AND kind=? AND key=?").run(args.resource, args.kind, args.key);
51
51
  return value;
52
52
  } },
53
- "claudeAppOAuth.issue": { idempotent: false, fn: (args: Put & { refreshKey?: string; clientId: string }) => {
53
+ "claudeAppOAuth.issue": { offLoop: true, idempotent: false, fn: (args: Put & { refreshKey?: string; clientId: string }) => {
54
54
  // Consume and replace in the same writer transaction; concurrent refreshes cannot resurrect a revoked family.
55
55
  let old: { clientId: string; family: string; type: string; used?: boolean } | undefined;
56
56
  if (args.refreshKey) {
@@ -65,7 +65,7 @@ export const CLAUDE_APP_OAUTH_WRITE_OPS: Readonly<Record<string, WriterOpSpec>>
65
65
  .run(JSON.stringify({ ...old, used: true }), args.resource, args.kind, args.refreshKey!);
66
66
  return "ok";
67
67
  } },
68
- "claudeAppOAuth.revoke": { idempotent: true, fn: (args: Key & { clientId: string }) => {
68
+ "claudeAppOAuth.revoke": { offLoop: true, idempotent: true, fn: (args: Key & { clientId: string }) => {
69
69
  const value = get(args);
70
70
  const credential = value ? JSON.parse(value) as { clientId: string; family: string } : null;
71
71
  if (credential?.clientId === args.clientId) removeFamily(args.resource, credential.family);
@@ -3,16 +3,19 @@ import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
3
3
 
4
4
  export const CODEX_RESET_CONSUMES_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
5
5
  "codexResetConsumes.begin": {
6
+ offLoop: true,
6
7
  idempotent: true,
7
8
  fn: (args: { input: Parameters<typeof beginCodexResetConsume>[0] }, ctx: WriterOpContext) =>
8
9
  beginCodexResetConsume({ ...args.input, now: args.input.now ?? ctx.now }),
9
10
  },
10
11
  "codexResetConsumes.complete": {
12
+ offLoop: true,
11
13
  idempotent: true,
12
14
  fn: (args: { input: Parameters<typeof completeCodexResetConsume>[0] }, ctx: WriterOpContext) =>
13
15
  completeCodexResetConsume({ ...args.input, now: args.input.now ?? ctx.now }),
14
16
  },
15
17
  "codexResetConsumes.prune": {
18
+ offLoop: true,
16
19
  idempotent: true,
17
20
  fn: (args: { olderThan?: number }, ctx: WriterOpContext) => pruneCodexResetConsumes(args.olderThan ?? ctx.now),
18
21
  },
@@ -41,13 +41,15 @@ export interface CommandRow {
41
41
  }
42
42
 
43
43
 
44
- export function rowToCommand(row: CommandRow): Command {
44
+ /** `slim` (#3391): listings omit `params.agentProfile` (~0.5 MB per spawn row) instead of hydrating it;
45
+ * `GET /api/commands/:id` returns the full row. */
46
+ export function rowToCommand(row: CommandRow, options: { slim?: boolean } = {}): Command {
45
47
  return {
46
48
  id: row.id,
47
49
  type: row.type,
48
50
  source: row.source,
49
51
  target: row.target,
50
- params: hydrateCommandAgentProfile(parseObject(row.params), row.agent_profile_hash),
52
+ params: options.slim ? withoutAgentProfile(parseObject(row.params)) : hydrateCommandAgentProfile(parseObject(row.params), row.agent_profile_hash),
51
53
  status: row.status,
52
54
  result: row.result ? parseObject(row.result) : undefined,
53
55
  error: row.error ?? undefined,
@@ -78,6 +80,12 @@ export function rowToCommand(row: CommandRow): Command {
78
80
  };
79
81
  }
80
82
 
83
+ function withoutAgentProfile(params: Record<string, unknown>): Record<string, unknown> {
84
+ if (!("agentProfile" in params)) return params;
85
+ const { agentProfile: _omitted, ...rest } = params;
86
+ return rest;
87
+ }
88
+
81
89
  function parseObject(raw: string): Record<string, unknown> {
82
90
  try {
83
91
  const parsed = JSON.parse(raw);
@@ -9,6 +9,8 @@ export interface CommandFilters {
9
9
  dispatchableOnly?: boolean;
10
10
  /** #2663 — oldest-first for the executor DISPATCH feed, whose window is capped. See `getCommands`. */
11
11
  oldestFirst?: boolean;
12
+ /** #3391 — omit `params.agentProfile` from rows (listing views; fetch the row by id for the profile). */
13
+ slim?: boolean;
12
14
  }
13
15
 
14
16
  export function buildListCommandsQuery(filters: CommandFilters = {}): { sql: string; params: unknown[] } {
@@ -29,5 +29,5 @@ export const IN_FLIGHT_SPAWNS_BY_PARENT_SQL = `SELECT * FROM commands INDEXED BY
29
29
  /** One parent's agent.spawn commands still in flight, newest first. */
30
30
  export function listInFlightSpawnCommands(parentId: string, limit = 500): Command[] {
31
31
  const rows = getDb().query(IN_FLIGHT_SPAWNS_BY_PARENT_SQL).all(...SPAWN_IN_FLIGHT_STATUSES, parentId, limit) as CommandRow[];
32
- return rows.map(rowToCommand);
32
+ return rows.map((row) => rowToCommand(row));
33
33
  }