@humwick/server 0.130.2 → 0.130.3

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 (570) hide show
  1. package/bundled/prompts/manifest.json +1 -0
  2. package/bundled/prompts/task.hydration.md +1 -1
  3. package/bundled/roles/builder.md +1 -1
  4. package/docs/guides/sub-coordinators.md +12 -5
  5. package/docs/guides/tools-coordination.md +1 -0
  6. package/docs/guides/workflows.md +16 -0
  7. package/docs/openapi.json +73 -4
  8. package/package.json +8 -7
  9. package/public/assets/{MessageBubble-BWLqMGKG.js → MessageBubble-Bq9kr21N.js} +2 -2
  10. package/public/assets/{PlanGraphPanel-3TlIQpjk.js → PlanGraphPanel-Dqiroy06.js} +2 -2
  11. package/public/assets/{TraceView-Wimm0Czv.js → TraceView-DYU962-H.js} +2 -2
  12. package/public/assets/{VaultDecisionCard-Bau_vNax.js → VaultDecisionCard-DhPfzioE.js} +2 -2
  13. package/public/assets/{activity-CrSurn-r.js → activity-DzCcQTee.js} +2 -2
  14. package/public/assets/{agent-lineage-CknAJKcb.js → agent-lineage-SgDt9i3T.js} +2 -2
  15. package/public/assets/agent-liveness-Bq-s73jW.js +2 -0
  16. package/public/assets/{agent-name-DKbiIhBo.js → agent-name-CcY3Aijy.js} +2 -2
  17. package/public/assets/{agent-profiles-DwF6EQ4R.js → agent-profiles-cxtRBn6O.js} +2 -2
  18. package/public/assets/{agent-project-badge-Diz4Sjjb.js → agent-project-badge-DGYbIhaI.js} +2 -2
  19. package/public/assets/{agent-tools-CRhpRQza.js → agent-tools-DHbftV0N.js} +2 -2
  20. package/public/assets/{agent-type-icon-LrUEU3lB.js → agent-type-icon-CY113sp-.js} +2 -2
  21. package/public/assets/{agents-B-v66C-4.js → agents-BPzY7IlQ.js} +2 -2
  22. package/public/assets/{analytics-BqCb-LKi.js → analytics-DENFPv_a.js} +2 -2
  23. package/public/assets/{artifact-viewer-kf4pGuAR.js → artifact-viewer-KRvSc7sl.js} +2 -2
  24. package/public/assets/{artifacts-DJevXcvg.js → artifacts-C9gSmL3s.js} +2 -2
  25. package/public/assets/{artifacts-D32zyw4Y.js → artifacts-DT-HkI5O.js} +2 -2
  26. package/public/assets/{automation-BpRajx_5.js → automation-BsmjQhKD.js} +2 -2
  27. package/public/assets/{backlog-D_UjTdzX.js → backlog-8NYH_62t.js} +2 -2
  28. package/public/assets/{busy-health-N3VEh4ao.js → busy-health-D3hwvYb4.js} +2 -2
  29. package/public/assets/{channels-9jopungq.js → channels-DCJ5WL7O.js} +2 -2
  30. package/public/assets/{chat-CSXrTB3C.js → chat-DAIl357U.js} +3 -3
  31. package/public/assets/{code-preview-p1NnmqK0.js → code-preview-CzrZfr9U.js} +2 -2
  32. package/public/assets/{connectors-CZdqlDNL.js → connectors-xTxtlyou.js} +2 -2
  33. package/public/assets/{cosmos-BgdsE4dG.js → cosmos-BpebFLK2.js} +2 -2
  34. package/public/assets/{crate-agent-30aXTKva.js → crate-agent-BHvI5wXC.js} +2 -2
  35. package/public/assets/{dependency-graph-B_jvw2GE.js → dependency-graph-DGT0o3wJ.js} +2 -2
  36. package/public/assets/{deploys-DoN6bAb5.js → deploys-hqWOHSHE.js} +2 -2
  37. package/public/assets/{display-BrKWFGJz.js → display-CDeH4I8T.js} +2 -2
  38. package/public/assets/{files-D6XEu8Ng.js → files-BTeGVmTg.js} +2 -2
  39. package/public/assets/{fleet-C7jQWbw2.js → fleet-DgXtNgCa.js} +2 -2
  40. package/public/assets/{fleet-metrics-BiDs3MLx.js → fleet-metrics-BHEt5yc_.js} +2 -2
  41. package/public/assets/{formatted-body-CQBwc1fb.js → formatted-body-MLtodyPp.js} +3 -3
  42. package/public/assets/{formatted-body-impl-DyG0WsQS.js → formatted-body-impl-CAe664Z8.js} +2 -2
  43. package/public/assets/{host-providers-dialog-DKaW31dN.js → host-providers-dialog-mapF4Fj9.js} +2 -2
  44. package/public/assets/{index-CRHep3M9.js → index-CqqgOI5R.js} +5 -5
  45. package/public/assets/{insights-CzrtkpOE.js → insights-DNyf7IzF.js} +2 -2
  46. package/public/assets/{integrations-aVsUmigt.js → integrations-DlYOHq7T.js} +2 -2
  47. package/public/assets/{issue-ref-chip-BnSlM_95.js → issue-ref-chip-CeWBNWn7.js} +2 -2
  48. package/public/assets/{log-viewer-qe7Ee47Z.js → log-viewer-Cib8nqdE.js} +2 -2
  49. package/public/assets/{maintenance-NfjqFZAk.js → maintenance-DSn1cPm7.js} +2 -2
  50. package/public/assets/{managed-agents-BiH0HgaE.js → managed-agents-C3CkM81m.js} +2 -2
  51. package/public/assets/{markdown-preview-BixaT3Xr.js → markdown-preview-W4VQQPAv.js} +3 -3
  52. package/public/assets/{markdown-preview-impl-DNGlWIS3.js → markdown-preview-impl-CMsTdhjq.js} +2 -2
  53. package/public/assets/{memory-DNsCl6UX.js → memory-BV17qtO4.js} +2 -2
  54. package/public/assets/{messages-DopU1L1X.js → messages-Bm_1AqSA.js} +2 -2
  55. package/public/assets/{orchestrators-Cq7ckLQT.js → orchestrators-Dg-vfP09.js} +2 -2
  56. package/public/assets/{overview-C1DCTzzV.js → overview-DcYCrvlr.js} +2 -2
  57. package/public/assets/{pairs-DE_m6rrE.js → pairs-8o9nuG3V.js} +2 -2
  58. package/public/assets/{profile-icon-B3rfh77F.js → profile-icon-BsRyBMea.js} +2 -2
  59. package/public/assets/{project-profile-instructions-DQWZE0kA.js → project-profile-instructions-DETKFn_E.js} +2 -2
  60. package/public/assets/{projects-Br2Bd2h1.js → projects-M_m6AHws.js} +2 -2
  61. package/public/assets/{prompt-settings-BKvFsvGD.js → prompt-settings-C_mNCgeL.js} +2 -2
  62. package/public/assets/provider-display-BEeLXC_D.js +2 -0
  63. package/public/assets/{registry-Cw38_6gW.js → registry-DDHHHE5N.js} +2 -2
  64. package/public/assets/run-card-Dt4I4jaP.js +2 -0
  65. package/public/assets/{secrets-DxI-vzSq.js → secrets-BkVwxI5O.js} +2 -2
  66. package/public/assets/{service-access-settings-D9s_VJbd.js → service-access-settings-Crq-6a-B.js} +2 -2
  67. package/public/assets/{settings-CKZTVGIx.js → settings-CYcavMFu.js} +2 -2
  68. package/public/assets/shipping-H6gzEeZB.js +2 -0
  69. package/public/assets/{stats-DyUBKyWI.js → stats-BJVeWkpV.js} +2 -2
  70. package/public/assets/{status-dot-DYqlEUhJ.js → status-dot-BcHyw1Vw.js} +2 -2
  71. package/public/assets/store-DKPDIZBe.js +9 -0
  72. package/public/assets/{tasks-Bh7Chr4-.js → tasks-qJjlQPpx.js} +2 -2
  73. package/public/assets/{teams-bowI7qVY.js → teams-Cd5Sk1uX.js} +2 -2
  74. package/public/assets/{terminal-viewer-BgVLHdsU.js → terminal-viewer-CpY5_m7W.js} +3 -3
  75. package/public/assets/{terminal-viewer-impl-Dz22UPy6.js → terminal-viewer-impl-DI_2llFR.js} +2 -2
  76. package/public/assets/{use-agent-terminal-DSQQ-iY7.js → use-agent-terminal-0yJA_buM.js} +2 -2
  77. package/public/assets/{use-decisions-samMQhQf.js → use-decisions-uJXHf34W.js} +2 -2
  78. package/public/assets/{use-project-list-59LheVP7.js → use-project-list-CpWl6eHQ.js} +2 -2
  79. package/public/assets/{use-selectors-BZa25xEB.js → use-selectors-BdxBW8mL.js} +2 -2
  80. package/public/assets/{user-avatar-DHxlvUvV.js → user-avatar-3IndfpOc.js} +2 -2
  81. package/public/assets/{versioned-prompt-editor-BmED5DnJ.js → versioned-prompt-editor-CIVg7XB1.js} +2 -2
  82. package/public/assets/{work-queue-CSvpuNLz.js → work-queue-B9rnLwHZ.js} +2 -2
  83. package/public/assets/{workflow-marker-DAZcj5qt.js → workflow-marker-X8wxyhzC.js} +2 -2
  84. package/public/assets/{workflows-DdmxRSyg.js → workflows-o5YyMFW7.js} +2 -2
  85. package/public/assets/{workspaces-BxeKfVuF.js → workspaces-CzEGoAng.js} +2 -2
  86. package/public/cosmos.html +4 -4
  87. package/public/index.html +28 -28
  88. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  89. package/runner/src/adapters/claude-headless.ts +1 -1
  90. package/runner/src/adapters/claude-launch-exit.ts +26 -0
  91. package/runner/src/adapters/claude-session-probe.ts +1 -1
  92. package/runner/src/adapters/claude-tmux.ts +38 -10
  93. package/runner/src/adapters/claude.ts +28 -75
  94. package/runner/src/adapters/provider-quota-harvest.ts +20 -71
  95. package/runner/src/agent-runner/lifecycle-milestones.ts +2 -2
  96. package/runner/src/agent-runner/liveness-exchange.ts +104 -0
  97. package/runner/src/claude-system-prompt.ts +49 -0
  98. package/runner/src/launch-assembly.ts +14 -10
  99. package/runner/src/profile-home.ts +1 -1
  100. package/runner/src/project-instructions.ts +2 -2
  101. package/runner/src/provider-command.ts +18 -0
  102. package/runner/src/provider-setup.ts +2 -1
  103. package/runner/src/runner-core.ts +14 -15
  104. package/runner/src/runner-helpers.ts +0 -8
  105. package/runner/src/session-scratch.ts +6 -2
  106. package/runner/test-support/provisioning-context.ts +4 -2
  107. package/runner/test-support/runner-harness.ts +2 -1
  108. package/scripts/steward-policy-cleanup.ts +2 -2
  109. package/src/agent-authz.ts +2 -2
  110. package/src/agent-drive-arbiter.ts +3 -8
  111. package/src/agent-exit-cleanup-async.ts +2 -2
  112. package/src/agent-exit-cleanup.ts +23 -19
  113. package/src/agent-lifecycle-events.ts +112 -85
  114. package/src/agent-ref.ts +3 -2
  115. package/src/agent-status-events.ts +43 -14
  116. package/src/auth/password.ts +4 -3
  117. package/src/auth/session-refresh.ts +1 -1
  118. package/src/auth/session.test-fixture.ts +2 -2
  119. package/src/auth/session.ts +7 -7
  120. package/src/automations/crud.ts +8 -81
  121. package/src/automations/dispatch.ts +33 -58
  122. package/src/automations/index.ts +1 -1
  123. package/src/automations/persistence.ts +65 -0
  124. package/src/automations/precheck.ts +18 -16
  125. package/src/automations/read-async.ts +11 -0
  126. package/src/automations/reconcile.ts +30 -39
  127. package/src/automations/storage.ts +33 -10
  128. package/src/automations/write-async.ts +33 -0
  129. package/src/branch-land-notice.ts +3 -3
  130. package/src/branch-landed.ts +25 -23
  131. package/src/bus.ts +21 -19
  132. package/src/cd-deploy-notify.ts +2 -2
  133. package/src/ci/notifications.ts +4 -4
  134. package/src/ci/root-missing.ts +3 -3
  135. package/src/ci/transition-notify.ts +5 -5
  136. package/src/claude-app/oauth-http.ts +3 -3
  137. package/src/cli/workflow.ts +12 -3
  138. package/src/commands-db.ts +4 -4
  139. package/src/commands-write.ts +8 -0
  140. package/src/compaction-watch.ts +10 -8
  141. package/src/config-store-write.ts +2 -2
  142. package/src/config.ts +4 -11
  143. package/src/connectors.ts +10 -8
  144. package/src/context-advisory.ts +22 -21
  145. package/src/db/agent-action-write.ts +36 -0
  146. package/src/db/agent-control-async.ts +9 -0
  147. package/src/db/agent-control-write.ts +49 -0
  148. package/src/db/agent-effort-observe.ts +22 -0
  149. package/src/db/agent-meta.ts +14 -3
  150. package/src/db/agents-async.ts +8 -5
  151. package/src/db/agents.ts +15 -12
  152. package/src/db/auth-identities.ts +2 -2
  153. package/src/db/automation-read-ops.ts +11 -0
  154. package/src/db/automation-write-ops.ts +46 -0
  155. package/src/db/blackboard.ts +2 -2
  156. package/src/db/browser-session-write.ts +32 -0
  157. package/src/db/bus-outbox-write-ops.ts +3 -1
  158. package/src/db/checkout-refresh-schema.ts +47 -0
  159. package/src/db/checkout-refresh.ts +5 -5
  160. package/src/db/command-agent-profile-backfill.ts +39 -0
  161. package/src/db/command-agent-profiles.ts +5 -1
  162. package/src/db/command-retention.ts +126 -0
  163. package/src/db/commands-planner-stats.test-fixture.ts +8 -0
  164. package/src/db/commands-read-ops.ts +16 -0
  165. package/src/db/connection.ts +1 -182
  166. package/src/db/context-carry-write.ts +74 -0
  167. package/src/db/continuations.ts +4 -4
  168. package/src/db/db-error-registry.ts +12 -0
  169. package/src/db/gateway.ts +26 -133
  170. package/src/db/history-retention-ops.ts +13 -0
  171. package/src/db/inbox-async.ts +2 -2
  172. package/src/db/inbox-read-ops.ts +1 -1
  173. package/src/db/inbox.ts +9 -3
  174. package/src/db/issue-associations.ts +2 -2
  175. package/src/db/issue-refs-async.ts +4 -1
  176. package/src/db/issue-refs-read-ops.ts +6 -0
  177. package/src/db/issue-refs.ts +8 -8
  178. package/src/db/kernel-event-log-schema.ts +2 -0
  179. package/src/db/land-boundaries.ts +51 -5
  180. package/src/db/land-gate-attestations.ts +2 -2
  181. package/src/db/landing-receipts.ts +2 -2
  182. package/src/db/lifecycle-context-write.ts +5 -0
  183. package/src/db/lifecycle-tail-async.ts +76 -0
  184. package/src/db/lifecycle-tail-write-ops.ts +62 -0
  185. package/src/db/lifecycle-write-window.ts +2 -1
  186. package/src/db/lineage-pair-dedupe-sql.ts +3 -3
  187. package/src/db/main-connection-mode.ts +11 -0
  188. package/src/db/main-connection-writes.ts +20 -2
  189. package/src/db/maintenance-job-runs-async.ts +2 -2
  190. package/src/db/maintenance-job-write-ops.ts +1 -1
  191. package/src/db/maintenance-pauses.ts +2 -2
  192. package/src/db/message-send.ts +4 -0
  193. package/src/db/messages.ts +31 -25
  194. package/src/db/migrations.ts +18 -1
  195. package/src/db/notification-write-ops.ts +19 -0
  196. package/src/db/operator-answer-write.ts +19 -0
  197. package/src/db/orchestrators.ts +10 -4
  198. package/src/db/pairs-async.ts +22 -1
  199. package/src/db/pairs-write-ops.ts +8 -3
  200. package/src/db/pairs.ts +41 -48
  201. package/src/db/plan-dispatch-schema.ts +5 -1
  202. package/src/db/plan-ready-notices.ts +43 -0
  203. package/src/db/plan-tools-include-migration.ts +36 -0
  204. package/src/db/plans-write-ops.ts +2 -0
  205. package/src/db/presence-buffer.ts +5 -0
  206. package/src/db/presence-loop.ts +18 -23
  207. package/src/db/presence-write.ts +0 -6
  208. package/src/db/project-entries-async.ts +14 -0
  209. package/src/db/project-entries-read-ops.ts +5 -1
  210. package/src/db/project-entries-write-ops.ts +8 -0
  211. package/src/db/project-entries.ts +15 -12
  212. package/src/db/project-journal.ts +2 -2
  213. package/src/db/project-trust-approvals.ts +2 -2
  214. package/src/db/prompt-templates.ts +4 -4
  215. package/src/db/read-op-marshaling-fixtures.ts +18 -0
  216. package/src/db/read-ops.ts +4 -0
  217. package/src/db/retention.ts +13 -2
  218. package/src/db/review-publish-write.ts +8 -0
  219. package/src/db/runtime-admin-async.ts +19 -0
  220. package/src/db/runtime-admin-write-ops.ts +36 -0
  221. package/src/db/runtime-tail-async.ts +288 -0
  222. package/src/db/runtime-tail-write-ops.ts +97 -0
  223. package/src/db/schema.ts +4 -1
  224. package/src/db/spawn-budget.ts +5 -0
  225. package/src/db/spawn-delegation.ts +1 -11
  226. package/src/db/sql-audit-indexes.ts +3 -0
  227. package/src/db/stats-async.ts +5 -3
  228. package/src/db/task-entity.ts +5 -4
  229. package/src/db/team-claim-policy.ts +60 -0
  230. package/src/db/teams-async.ts +4 -0
  231. package/src/db/teams-write-ops.ts +4 -0
  232. package/src/db/terminal-events.ts +15 -0
  233. package/src/db/timer-cutover-bodies.ts +292 -0
  234. package/src/db/timer-cutover-core.ts +37 -278
  235. package/src/db/timer-cutover-state.ts +40 -0
  236. package/src/db/timer-cutover-write-ops.ts +1 -40
  237. package/src/db/token-db-write-ops.ts +2 -0
  238. package/src/db/token-standing-async.ts +12 -0
  239. package/src/db/token-standing.ts +6 -9
  240. package/src/db/token-usage-retention.ts +33 -0
  241. package/src/db/token-usage-write-ops.ts +1 -1
  242. package/src/db/token-usage.ts +4 -1
  243. package/src/db/user-invites-async.ts +13 -0
  244. package/src/db/user-invites.ts +3 -3
  245. package/src/db/users.ts +14 -6
  246. package/src/db/workspaces.ts +8 -5
  247. package/src/db/write-ops.ts +31 -12
  248. package/src/db/writer-fault-seam.ts +2 -9
  249. package/src/db/writer-fault-test-ops.ts +18 -0
  250. package/src/db/writer-gateway.ts +42 -46
  251. package/src/dev.ts +8 -3
  252. package/src/event-loop-lag.ts +4 -2
  253. package/src/event-loop-yield.ts +2 -2
  254. package/src/events.ts +10 -8
  255. package/src/host-provider-store.ts +5 -4
  256. package/src/hub-metrics-report.ts +5 -1
  257. package/src/hub-metrics.ts +38 -18
  258. package/src/injection-sweep-pacing.ts +6 -8
  259. package/src/land-gate-notifications.ts +11 -10
  260. package/src/land-gate-queue-progress.ts +3 -3
  261. package/src/land-settle-dispatch.ts +52 -92
  262. package/src/lifecycle-context-pressure.ts +11 -10
  263. package/src/lifecycle-manager.ts +104 -98
  264. package/src/maintenance/breaker.ts +18 -16
  265. package/src/maintenance/checkout-refresh.ts +18 -1
  266. package/src/maintenance/critical-job-watchdog.ts +3 -3
  267. package/src/maintenance/events.ts +3 -3
  268. package/src/maintenance/history-retention-job.ts +30 -0
  269. package/src/maintenance/jobs.ts +29 -67
  270. package/src/maintenance/long-running-work.ts +3 -1
  271. package/src/maintenance/offline-agent-prune-job.ts +28 -0
  272. package/src/maintenance/orphaned-sessions.ts +5 -4
  273. package/src/maintenance/pauses.ts +4 -10
  274. package/src/maintenance/plan-ready-notice-job.ts +71 -0
  275. package/src/maintenance/scheduler.ts +18 -13
  276. package/src/maintenance/stalled-delivery-pager.ts +4 -4
  277. package/src/maintenance/teams.ts +19 -30
  278. package/src/maintenance/workspaces.ts +17 -27
  279. package/src/managed-policy-escalation.ts +7 -7
  280. package/src/managed-policy.ts +3 -3
  281. package/src/managed-provider-gate.ts +5 -4
  282. package/src/managed-spawn-reservation.ts +5 -4
  283. package/src/mcp/index.ts +8 -8
  284. package/src/mcp/output-descriptions.ts +1 -1
  285. package/src/mcp/presentation.ts +2 -1
  286. package/src/mcp/runtime-output.ts +6 -1
  287. package/src/mcp/self-care-visibility.ts +2 -2
  288. package/src/mcp/spawn-targets-matrix.ts +10 -1
  289. package/src/mcp/tool-groups.ts +3 -2
  290. package/src/mcp/tool-roles.ts +81 -0
  291. package/src/mcp/tools-workspace.ts +1 -1
  292. package/src/mcp-blackboard-tools.ts +6 -5
  293. package/src/mcp-decision-tools.ts +7 -7
  294. package/src/mcp-egress-tools.ts +10 -9
  295. package/src/mcp-issue-activity-tools.ts +2 -2
  296. package/src/mcp-issue-association-tools.ts +4 -4
  297. package/src/mcp-issue-ref-tools.ts +3 -3
  298. package/src/mcp-merge-tools.ts +5 -5
  299. package/src/mcp-notification-subscription-tools.ts +14 -15
  300. package/src/mcp-output-levels.test-fixture.ts +1 -1
  301. package/src/mcp-profile-trust-tools.ts +1 -1
  302. package/src/mcp-project-journal-tools.ts +4 -12
  303. package/src/mcp-project-tools.ts +17 -18
  304. package/src/mcp-routing-policy-tools.ts +3 -2
  305. package/src/mcp-team-tools.ts +6 -6
  306. package/src/mcp-workflow-tools.ts +5 -3
  307. package/src/memory-area-injection.ts +13 -12
  308. package/src/memory-boundary-capture.ts +6 -6
  309. package/src/memory-injection.ts +45 -43
  310. package/src/merge-pause-maintenance.ts +5 -4
  311. package/src/notification-router.ts +44 -17
  312. package/src/notify.ts +5 -5
  313. package/src/observed-reset-refresh.ts +4 -4
  314. package/src/operator-alarm.ts +2 -5
  315. package/src/project-ingest.ts +6 -6
  316. package/src/project-instructions.ts +8 -1
  317. package/src/project-trust.ts +12 -11
  318. package/src/prompt-resolver.ts +3 -3
  319. package/src/provider-quota-async.ts +5 -7
  320. package/src/provider-timeline-event.ts +4 -7
  321. package/src/quota-band-delivery.ts +2 -2
  322. package/src/recipe-runner.ts +4 -3
  323. package/src/retention-config-store.ts +4 -3
  324. package/src/reviewer-pipeline.ts +11 -10
  325. package/src/routes/_route.ts +4 -4
  326. package/src/routes/_shared.ts +13 -12
  327. package/src/routes/agent-effort.ts +5 -5
  328. package/src/routes/agent-heartbeat-quota.ts +23 -0
  329. package/src/routes/agent-keep.ts +1 -1
  330. package/src/routes/agent-reporting.ts +1 -1
  331. package/src/routes/agent-self-effort.ts +5 -5
  332. package/src/routes/agent-sessions.ts +24 -24
  333. package/src/routes/agents-spawn.ts +1 -1
  334. package/src/routes/agents.ts +17 -119
  335. package/src/routes/auth.ts +14 -15
  336. package/src/routes/authority-audit.ts +2 -2
  337. package/src/routes/automations.ts +6 -5
  338. package/src/routes/channel-binding-delete.ts +1 -1
  339. package/src/routes/channel-output.ts +3 -3
  340. package/src/routes/claude-api-budget.ts +5 -5
  341. package/src/routes/commands-listing.ts +1 -1
  342. package/src/routes/commands.ts +29 -27
  343. package/src/routes/context-resets.ts +2 -2
  344. package/src/routes/coordinator-teams.ts +1 -1
  345. package/src/routes/decisions.ts +2 -2
  346. package/src/routes/host-secret-files.ts +1 -1
  347. package/src/routes/hosts.ts +11 -11
  348. package/src/routes/inbox.ts +3 -2
  349. package/src/routes/index.ts +2 -1
  350. package/src/routes/integrations.ts +9 -7
  351. package/src/routes/issue-activity.ts +2 -2
  352. package/src/routes/issue-associations.ts +4 -4
  353. package/src/routes/issue-refs.ts +3 -3
  354. package/src/routes/kernel-timers-operator.ts +1 -1
  355. package/src/routes/memory.ts +1 -1
  356. package/src/routes/merges.ts +11 -11
  357. package/src/routes/messages.ts +23 -17
  358. package/src/routes/orchestrator-bootstrap.ts +1 -1
  359. package/src/routes/orchestrator-host-registration.ts +2 -0
  360. package/src/routes/orchestrator-host-utilization.ts +2 -2
  361. package/src/routes/orchestrator-proxy.ts +2 -2
  362. package/src/routes/orchestrator.ts +27 -18
  363. package/src/routes/pairs.ts +8 -8
  364. package/src/routes/project-profile-instructions.ts +1 -1
  365. package/src/routes/project-trust.ts +1 -1
  366. package/src/routes/projects.ts +16 -16
  367. package/src/routes/prompt-templates.ts +12 -12
  368. package/src/routes/provider-quotas.ts +10 -10
  369. package/src/routes/run-command.ts +1 -1
  370. package/src/routes/secrets-backup.ts +2 -2
  371. package/src/routes/secrets-import.ts +1 -1
  372. package/src/routes/secrets-vault.ts +7 -7
  373. package/src/routes/settings.ts +1 -1
  374. package/src/routes/shipping.ts +9 -1
  375. package/src/routes/spawn-policy.ts +8 -7
  376. package/src/routes/sse.ts +2 -2
  377. package/src/routes/team-templates.ts +7 -7
  378. package/src/routes/teams.ts +10 -10
  379. package/src/routes/tokens.ts +18 -17
  380. package/src/routes/users.ts +24 -24
  381. package/src/routes/viewer-read.ts +3 -3
  382. package/src/routes/web-tool-calls.ts +4 -4
  383. package/src/routes/workflows.ts +2 -2
  384. package/src/routes/workspaces.ts +17 -16
  385. package/src/routing-policy-push.ts +6 -6
  386. package/src/runtime-grant.ts +17 -11
  387. package/src/runtime-tokens.ts +20 -28
  388. package/src/secrets-broker/config.ts +1 -1
  389. package/src/secrets-broker/service.ts +2 -2
  390. package/src/security.ts +3 -1
  391. package/src/self-resume-carry.ts +21 -110
  392. package/src/self-resume-supersede.ts +4 -3
  393. package/src/self-resume.ts +54 -66
  394. package/src/server-background.ts +1 -1
  395. package/src/server.ts +21 -22
  396. package/src/services/agent-action.ts +12 -31
  397. package/src/services/agent-effort.ts +7 -32
  398. package/src/services/agent-hang-watchdog-job.ts +2 -2
  399. package/src/services/agent-heartbeat.ts +166 -0
  400. package/src/services/agent-reporting.ts +1 -1
  401. package/src/services/browser-inventory.ts +3 -3
  402. package/src/services/browser-session-state.ts +3 -2
  403. package/src/services/browser-session.ts +21 -27
  404. package/src/services/bus-command-update.ts +9 -9
  405. package/src/services/checkout-refresh-state.ts +1 -0
  406. package/src/services/checkout-refresh.test-fixture.ts +3 -2
  407. package/src/services/command-host.ts +7 -2
  408. package/src/services/coordinator-digest.ts +3 -2
  409. package/src/services/dispatch-command.ts +12 -13
  410. package/src/services/effective-launch-driver.ts +14 -9
  411. package/src/services/host-changed-paths.test-fixture.ts +5 -6
  412. package/src/services/host-project-readiness.ts +2 -2
  413. package/src/services/host-providers.ts +1 -1
  414. package/src/services/host-utilization.ts +9 -9
  415. package/src/services/idle-agent-digest.ts +17 -12
  416. package/src/services/idle-epics.ts +4 -4
  417. package/src/services/interrupt-agent.ts +12 -9
  418. package/src/services/issue-activity.ts +3 -3
  419. package/src/services/issue-associations.ts +15 -18
  420. package/src/services/issue-lifecycle-comments.ts +3 -2
  421. package/src/services/issue-lifecycle.ts +14 -13
  422. package/src/services/issue-refs.ts +4 -5
  423. package/src/services/kernel-timers-cutover-chaos-worker.ts +1 -1
  424. package/src/services/kernel-timers-ingress-chaos-worker.ts +6 -1
  425. package/src/services/kernel-timers-ingress.ts +3 -3
  426. package/src/services/kernel-timers-message-authority-chaos-worker.ts +3 -10
  427. package/src/services/kernel-timers-operator-chaos-worker.ts +3 -2
  428. package/src/services/kernel-timers-outbox.ts +1 -1
  429. package/src/services/kernel-timers-stage10-operator.ts +1 -1
  430. package/src/services/kernel-timers.ts +4 -3
  431. package/src/services/land-gate-placement.ts +11 -2
  432. package/src/services/land-issue-relations.ts +5 -4
  433. package/src/services/land-restart-retry.ts +7 -6
  434. package/src/services/landed-ancestry.ts +6 -5
  435. package/src/services/legacy-landing-host.ts +4 -1
  436. package/src/services/legacy-landing-recovery.ts +8 -6
  437. package/src/services/managed-running.ts +17 -17
  438. package/src/services/memory-areas.ts +3 -3
  439. package/src/services/merge-pause.ts +9 -15
  440. package/src/services/operator-decisions.ts +37 -41
  441. package/src/services/orchestrator-registration-recovery.ts +6 -2
  442. package/src/services/orchestrator-upgrade.ts +14 -12
  443. package/src/services/permission-decision.ts +4 -3
  444. package/src/services/plan-ci-notice.ts +1 -1
  445. package/src/services/plan-conformance.ts +4 -4
  446. package/src/services/plan-dispatch-policy.ts +10 -3
  447. package/src/services/plan-dispatch.ts +35 -14
  448. package/src/services/plan-graph.ts +39 -261
  449. package/src/services/plan-import.ts +1 -1
  450. package/src/services/plan-live-bindings.ts +8 -37
  451. package/src/services/plan-task-create.ts +1 -1
  452. package/src/services/plan-terminal-refs.ts +2 -12
  453. package/src/services/plan-wake-digest.ts +8 -3
  454. package/src/services/profile-trust.ts +2 -2
  455. package/src/services/project.ts +11 -10
  456. package/src/services/prompt-templates.ts +6 -5
  457. package/src/services/provider-hold-nudge.ts +9 -9
  458. package/src/services/provider-hold-record.ts +13 -12
  459. package/src/services/provider-state-audit.ts +19 -18
  460. package/src/services/provisioning-harness/driver.ts +1 -1
  461. package/src/services/provisioning-harness/fixture-matrix.ts +3 -3
  462. package/src/services/provisioning-harness/lifecycle.ts +24 -22
  463. package/src/services/provisioning-harness/prepare.ts +16 -17
  464. package/src/services/rate-limit-resume.ts +10 -9
  465. package/src/services/red-main-auto-revert.ts +7 -5
  466. package/src/services/red-main-controller-deps.ts +20 -3
  467. package/src/services/red-main-incident.ts +3 -1
  468. package/src/services/red-main-quarantine.ts +2 -3
  469. package/src/services/register-agent.ts +22 -22
  470. package/src/services/restart-project-role.ts +1 -1
  471. package/src/services/restart-spawn.ts +17 -16
  472. package/src/services/review-publish.ts +7 -24
  473. package/src/services/review-ref-reconcile.ts +29 -38
  474. package/src/services/runtime-dependency-alert.ts +27 -0
  475. package/src/services/scheduler-command.ts +11 -9
  476. package/src/services/scheduler.ts +5 -5
  477. package/src/services/self-effort-grant.ts +5 -25
  478. package/src/services/send-message.ts +16 -33
  479. package/src/services/shutdown-agent.ts +17 -14
  480. package/src/services/spawn-agent.ts +12 -41
  481. package/src/services/spawn-chain-inspector.ts +1 -1
  482. package/src/services/spawn-cwd-placement.ts +21 -5
  483. package/src/services/spawn-issue-activity.ts +8 -6
  484. package/src/services/spawn-launch.ts +17 -1
  485. package/src/services/spawn-loss-escalation.ts +1 -1
  486. package/src/services/spawn-registration-watchdog.ts +2 -2
  487. package/src/services/spawn-repo-probe.ts +7 -5
  488. package/src/services/spawn-tree-cleanup.ts +7 -8
  489. package/src/services/steward-reaper.ts +4 -4
  490. package/src/services/task-context-hydration.ts +25 -1
  491. package/src/services/task-delivery.ts +20 -34
  492. package/src/services/task-entity.ts +40 -203
  493. package/src/services/task-issue-link.ts +13 -20
  494. package/src/services/task-issue-weave.ts +12 -11
  495. package/src/services/task-lifecycle.ts +27 -31
  496. package/src/services/task-repo-cwd.ts +37 -0
  497. package/src/services/task-review-hold.ts +4 -17
  498. package/src/services/task-semantic-events.ts +13 -11
  499. package/src/services/task-state-reconcile.ts +3 -4
  500. package/src/services/task-validation.ts +2 -2
  501. package/src/services/team-templates.ts +5 -5
  502. package/src/services/team.ts +31 -98
  503. package/src/services/terminal-provider-exit.ts +6 -5
  504. package/src/services/timer-message-fire-chaos-worker.ts +7 -3
  505. package/src/services/timer-message-fire-outbox.ts +1 -1
  506. package/src/services/transient-agent-reaper.ts +22 -19
  507. package/src/services/turn-reconcile-audit.ts +9 -9
  508. package/src/services/user-onboarding.ts +8 -6
  509. package/src/services/user-profile.ts +6 -9
  510. package/src/services/work-finality.ts +16 -4
  511. package/src/services/workflow-auto-start.ts +2 -1
  512. package/src/services/workflow-caller.ts +61 -4
  513. package/src/services/workflow-child-proof-worker.ts +1 -1
  514. package/src/services/workflow-control-proof-worker.ts +1 -1
  515. package/src/services/workflow-coordinator.ts +8 -2
  516. package/src/services/workflow-decisions.ts +5 -5
  517. package/src/services/workflow-deferred-cleanup.ts +1 -1
  518. package/src/services/workflow-effect-driver.ts +42 -19
  519. package/src/services/workflow-effects.ts +41 -27
  520. package/src/services/workflow-pins.ts +7 -4
  521. package/src/services/workflow-reconcile.ts +1 -0
  522. package/src/services/workflow-resume-pins.ts +3 -3
  523. package/src/services/workflow-routing-defaults.ts +18 -0
  524. package/src/services/workflow-run-view.ts +4 -3
  525. package/src/services/workflow-run.test-fixture.ts +1 -1
  526. package/src/services/workflow-runs.ts +34 -22
  527. package/src/services/workflow-supervisor-state.ts +21 -1
  528. package/src/services/workflow-supervisor.ts +4 -3
  529. package/src/services/workspace-recovery.ts +4 -3
  530. package/src/spawn-admission.ts +10 -2
  531. package/src/spawn-placement.ts +27 -9
  532. package/src/spawn-targets.ts +8 -8
  533. package/src/sse-agent-status.ts +15 -6
  534. package/src/sse.ts +49 -24
  535. package/src/startup-context-injection.ts +61 -57
  536. package/src/steward-handoff.ts +4 -3
  537. package/src/steward-policy-cleanup.ts +9 -8
  538. package/src/steward.ts +2 -2
  539. package/src/task-dispatch-live.ts +5 -2
  540. package/src/task-dispatcher.ts +2 -2
  541. package/src/token-db.ts +2 -2
  542. package/src/token-usage-async.ts +2 -4
  543. package/src/user-awaiting-reply.ts +5 -5
  544. package/src/workspace-actions.ts +24 -29
  545. package/src/workspace-auto-merge.ts +14 -12
  546. package/src/workspace-changed-paths-client.ts +16 -5
  547. package/src/workspace-cleanup-stale.ts +30 -7
  548. package/src/workspace-command-settle.ts +24 -16
  549. package/src/workspace-config.ts +1 -1
  550. package/src/workspace-conflict-resolver.ts +9 -7
  551. package/src/workspace-dead-owner-recovery.ts +5 -4
  552. package/src/workspace-escalation.ts +12 -11
  553. package/src/workspace-human-escalation.ts +3 -3
  554. package/src/workspace-land-gate-watchdog.ts +3 -3
  555. package/src/workspace-land-refusal.ts +20 -0
  556. package/src/workspace-land-stopped.ts +1 -0
  557. package/src/workspace-merge.ts +10 -26
  558. package/src/workspace-nothing-to-land-notice.ts +2 -2
  559. package/src/workspace-orphans.ts +18 -37
  560. package/src/workspace-pr-completion.ts +10 -8
  561. package/src/workspace-rekey.ts +3 -3
  562. package/src/workspace-steward-failure.ts +2 -2
  563. package/src/workspace-steward-wake.ts +2 -2
  564. package/public/assets/agent-liveness-CaNwK0Zf.js +0 -2
  565. package/public/assets/provider-display-DsuWMvVL.js +0 -2
  566. package/public/assets/run-card-CpKqRbhg.js +0 -2
  567. package/public/assets/shipping-B9YtnwtG.js +0 -2
  568. package/public/assets/store-DcClXLH0.js +0 -9
  569. package/runner/src/agent-runner/http-liveness.ts +0 -85
  570. package/src/db/on-loop-write-transaction.ts +0 -34
@@ -1,3 +1,7 @@
1
+ import { framed } from "../hub-metrics";
2
+ import { getGateway } from "./gateway";
3
+ import { normalizePhase, readCutover } from "./timer-cutover-state";
4
+ import type { CutoverWriterState, CutoverStepArgs, CutoverStepResults } from "./timer-cutover-bodies";
1
5
  import { randomUUID } from "node:crypto";
2
6
  import {
3
7
  clearExpiredLegacyMessageClaimsInTx,
@@ -64,48 +68,9 @@ export type TimerMessageCutoverTickResult =
64
68
  | { state: "absent" | "idle" | "waiting-for-lease" }
65
69
  | { state: "advanced" | "holding" | "aborting"; phase: TimerMessageCutoverPhase; epoch: number; liveClaims?: number; guard: TimerMessageDeadlineGuard };
66
70
 
67
- function normalizePhase(phase: string): TimerMessageCutoverPhase {
68
- return phase === "leased" || phase === "quiescing" || phase === "draining" || phase === "quiesced" || phase === "aborting"
69
- ? phase
70
- : "idle";
71
- }
72
-
73
- function journalOf(row: TimerMessageCutoverRow): TimerMessageCutoverJournalEntry[] {
74
- return Array.isArray(row.phaseJournal) ? row.phaseJournal as TimerMessageCutoverJournalEntry[] : [];
75
- }
76
-
77
- function appendJournal(row: TimerMessageCutoverRow, entry: TimerMessageCutoverJournalEntry): string {
78
- return JSON.stringify([...journalOf(row), entry]);
79
- }
80
-
81
- function readCutover(): TimerMessageCutoverRow | null {
82
- const row = getDb().query("SELECT * FROM timer_message_cutover WHERE id = 1").get() as any;
83
- return row ? {
84
- id: 1,
85
- protocolVersion: row.protocol_version,
86
- epoch: row.epoch,
87
- desiredMode: row.desired_mode,
88
- observedMode: row.observed_mode,
89
- phase: row.phase,
90
- leaseOwner: row.lease_owner,
91
- leaseExpiresAt: row.lease_expires_at,
92
- quiesceStartedAt: row.quiesce_started_at,
93
- quiesceDeadline: row.quiesce_deadline,
94
- lastCompletedTimer: row.last_completed_timer,
95
- ingressPolicy: row.ingress_policy,
96
- messageMutationGate: row.message_mutation_gate,
97
- gateEpoch: row.gate_epoch,
98
- abortRequested: row.abort_requested === 1,
99
- phaseJournal: JSON.parse(row.phase_journal),
100
- updatedAt: row.updated_at,
101
- } : null;
102
- }
103
-
104
71
  /** Singleton Stage-6 lease/quiesce state machine. It has no HTTP/MCP/CLI binding: production stays
105
72
  * shadow and the only boot wiring is recovery of an already-existing non-idle journal. */
106
- // LC-4c (#1920) owns extraction of this remaining synchronous state-machine core into a db-domain
107
- // writer-op module. It cannot be safely folded into this service in LC-4b: doing so breaches the
108
- // file-size ratchet; callers with an awaitable batch facade are already migrated here.
73
+ // #1870 — only transaction bodies cross the writer seam; fault hooks and timers stay on main.
109
74
  export class TimerMessageCutoverCoordinator {
110
75
  readonly ownerId: string;
111
76
  readonly #now: () => number;
@@ -158,7 +123,7 @@ export class TimerMessageCutoverCoordinator {
158
123
  if (!acquired) return false;
159
124
  const guard = this.deadlineGuard();
160
125
  if (!guard.safe) {
161
- this.requestAbort(`deadline guard refused entry (slack=${guard.slackMs}, required=${guard.requiredMs})`);
126
+ await this.requestAbort(`deadline guard refused entry (slack=${guard.slackMs}, required=${guard.requiredMs})`);
162
127
  await this.tick();
163
128
  await this.tick();
164
129
  return false;
@@ -166,15 +131,9 @@ export class TimerMessageCutoverCoordinator {
166
131
  return true;
167
132
  }
168
133
 
169
- requestAbort(reason = "abort requested"): boolean {
134
+ async requestAbort(reason = "abort requested"): Promise<boolean> {
170
135
  const now = this.#now();
171
- return writeTransaction(() => {
172
- const row = readCutover();
173
- if (!row || normalizePhase(row.phase) === "idle") return false;
174
- return getDb().query(`UPDATE timer_message_cutover
175
- SET abort_requested = 1, phase_journal = ?, updated_at = ? WHERE id = 1`)
176
- .run(appendJournal(row, { epoch: row.epoch, phase: "aborting", at: now, reason }), now).changes === 1;
177
- });
136
+ return await this.#writeStep("abort", { now, reason });
178
137
  }
179
138
 
180
139
  /** Stage 9: commit only the durable route marker while the same all-surface gate remains closed. */
@@ -182,19 +141,7 @@ export class TimerMessageCutoverCoordinator {
182
141
  const now = this.#now();
183
142
  const epoch = this.#epoch;
184
143
  if (epoch === undefined) throw new Error("cutover lease is not owned");
185
- writeTransaction(() => {
186
- const row = this.#owned(epoch, now);
187
- if (normalizePhase(row.phase) !== "quiesced" || row.messageMutationGate !== "closed") {
188
- throw new Error("ingress routing activation requires the live quiesced gate owner");
189
- }
190
- const changed = getDb().query(`UPDATE timer_message_cutover SET ingress_policy = 'ingress',
191
- phase_journal = ?, updated_at = ?
192
- WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?
193
- AND phase = 'quiesced' AND message_mutation_gate = 'closed'`)
194
- .run(appendJournal(row, { epoch, phase: "quiesced", at: now, reason: "authoritative ingress routing activated under fence" }),
195
- now, epoch, this.ownerId, now);
196
- if (changed.changes !== 1) throw new Error("cutover lease lost before ingress routing activation");
197
- });
144
+ await this.#writeStep("activate", { now, epoch });
198
145
  await this.#faults.afterRoutingActivation?.(epoch);
199
146
  }
200
147
 
@@ -205,23 +152,7 @@ export class TimerMessageCutoverCoordinator {
205
152
  const epoch = this.#epoch;
206
153
  if (epoch === undefined) throw new Error("cutover lease is not owned");
207
154
  await this.#faults.afterG1BeforeReopen?.(epoch);
208
- writeTransaction(() => {
209
- const row = this.#owned(epoch, now);
210
- if (normalizePhase(row.phase) !== "quiesced" || row.messageMutationGate !== "closed" || row.ingressPolicy !== "ingress") {
211
- throw new Error("G1 completion requires fenced authoritative ingress routing");
212
- }
213
- const changed = getDb().query(`UPDATE timer_message_cutover SET
214
- protocol_version = MAX(COALESCE(protocol_version, 1), 2),
215
- phase = 'idle', lease_owner = NULL, lease_expires_at = NULL,
216
- quiesce_started_at = NULL, quiesce_deadline = NULL,
217
- message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
218
- phase_journal = ?, updated_at = ?
219
- WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?
220
- AND phase = 'quiesced' AND message_mutation_gate = 'closed' AND ingress_policy = 'ingress'`)
221
- .run(appendJournal(row, { epoch, phase: "idle", at: now, reason: "G1 proven; gate reopened under legacy authority" }),
222
- now, epoch, this.ownerId, now);
223
- if (changed.changes !== 1) throw new Error("cutover lease lost before G1 gate reopen");
224
- });
155
+ await this.#writeStep("completeLegacy", { now, epoch });
225
156
  this.#epoch = undefined;
226
157
  }
227
158
 
@@ -236,37 +167,7 @@ export class TimerMessageCutoverCoordinator {
236
167
  const now = this.#now();
237
168
  const epoch = this.#epoch;
238
169
  if (epoch === undefined) throw new Error("cutover lease is not owned");
239
- writeTransaction(() => {
240
- const row = this.#owned(epoch, now);
241
- if (normalizePhase(row.phase) !== "quiesced" || row.messageMutationGate !== "closed"
242
- || row.ingressPolicy !== "ingress") {
243
- throw new Error("authority-transfer completion requires the live quiesced ingress gate owner");
244
- }
245
- // Only ACTIVE (scheduled) rows must converge on the target backend — terminal rows never
246
- // dispatch, so their backend is irrelevant and must not block completion (an ordinary kernel-mode
247
- // cancel terminalizes a kernel-owned row that a reverse would otherwise be unable to move).
248
- const wrongOwners = (getDb().query(`SELECT count(*) AS n FROM timer_message_authority
249
- WHERE lifecycle='scheduled' AND backend != ?`).get(expectedBackend) as { n: number }).n;
250
- if (wrongOwners !== 0) throw new Error(`${wrongOwners} scheduled MESSAGE owners are not ${expectedBackend}`);
251
- const expectedFrozen = expectedBackend === "kernel" ? 1 : 0;
252
- const projectionMismatch = (getDb().query(`SELECT count(*) AS n FROM timer_message_authority a
253
- JOIN scheduled_timers s ON s.id=a.timer_id
254
- WHERE a.lifecycle='scheduled' AND (s.command IS NOT NULL OR s.message_authority_frozen != ?)`)
255
- .get(expectedFrozen) as { n: number }).n;
256
- if (projectionMismatch !== 0) throw new Error(`${projectionMismatch} MESSAGE compatibility projections disagree with ${mode}`);
257
- const changed = getDb().query(`UPDATE timer_message_cutover SET
258
- desired_mode = ?, observed_mode = ?, phase = 'idle', lease_owner = NULL,
259
- lease_expires_at = NULL, quiesce_started_at = NULL, quiesce_deadline = NULL,
260
- message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
261
- phase_journal = ?, updated_at = ?
262
- WHERE id=1 AND epoch=? AND lease_owner=? AND lease_expires_at>?
263
- AND phase='quiesced' AND message_mutation_gate='closed' AND ingress_policy='ingress'`)
264
- .run(mode, mode, appendJournal(row, {
265
- epoch, phase: "idle", at: now,
266
- reason: `${mode} per-row ownership proven; MESSAGE mutation gate reopened`,
267
- }), now, epoch, this.ownerId, now);
268
- if (changed.changes !== 1) throw new Error("cutover lease lost before authority-transfer completion");
269
- });
170
+ await this.#writeStep("completeTransfer", { now, epoch, mode, expectedBackend });
270
171
  this.#epoch = undefined;
271
172
  await this.#faults.afterModeCommit?.(epoch, mode);
272
173
  }
@@ -275,22 +176,11 @@ export class TimerMessageCutoverCoordinator {
275
176
  * benignly refuse while reverse projection owns the quiesced writer window. `targetMode` is the mode
276
177
  * the reverse restores (dual-read→shadow or kernel→dual-read); `sourceObservedMode` is the currently
277
178
  * observed mode the reverse departs from. */
278
- markReverseTransferDesired(targetMode: KernelTimersDesiredMode, sourceObservedMode: KernelTimersDesiredMode): void {
179
+ async markReverseTransferDesired(targetMode: KernelTimersDesiredMode, sourceObservedMode: KernelTimersDesiredMode): Promise<void> {
279
180
  const now = this.#now();
280
181
  const epoch = this.#epoch;
281
182
  if (epoch === undefined) throw new Error("cutover lease is not owned");
282
- writeTransaction(() => {
283
- const row = this.#owned(epoch, now);
284
- const changed = getDb().query(`UPDATE timer_message_cutover SET desired_mode=?,
285
- phase_journal=?, updated_at=?
286
- WHERE id=1 AND epoch=? AND lease_owner=? AND lease_expires_at>?
287
- AND phase='quiesced' AND message_mutation_gate='closed' AND observed_mode=?`)
288
- .run(targetMode, appendJournal(row, {
289
- epoch, phase: "quiesced", at: now,
290
- reason: `${targetMode} rollback desired; kernel decision commits quiesced for reverse projection`,
291
- }), now, epoch, this.ownerId, now, sourceObservedMode);
292
- if (changed.changes !== 1) throw new Error(`reverse transfer requires observed ${sourceObservedMode} under the live fence`);
293
- });
183
+ await this.#writeStep("reverse", { now, epoch, targetMode, sourceObservedMode });
294
184
  }
295
185
 
296
186
  /** One deterministic recovery/coordination step. The deadline guard is recomputed on every call,
@@ -315,7 +205,7 @@ export class TimerMessageCutoverCoordinator {
315
205
  : boundedOut
316
206
  ? "bounded quiesce timeout reached"
317
207
  : `deadline guard tripped (slack=${guard.slackMs}, required=${guard.requiredMs})`;
318
- if (normalizePhase(row.phase) !== "aborting") this.#transition(row.epoch, "aborting", reason);
208
+ if (normalizePhase(row.phase) !== "aborting") await this.#transition(row.epoch, "aborting", reason);
319
209
  else await this.#finishAbort(row.epoch, reason);
320
210
  return { state: "aborting", phase: "aborting", epoch: row.epoch, guard };
321
211
  }
@@ -327,20 +217,20 @@ export class TimerMessageCutoverCoordinator {
327
217
 
328
218
  switch (normalizePhase(row.phase)) {
329
219
  case "leased":
330
- if (!this.#closeGate(row.epoch, now)) {
220
+ if (!await this.#closeGate(row.epoch, now)) {
331
221
  return { state: "aborting", phase: "aborting", epoch: row.epoch, guard: this.deadlineGuard(now) };
332
222
  }
333
223
  await this.#faults.afterGateClose?.(row.epoch);
334
224
  return { state: "advanced", phase: "quiescing", epoch: row.epoch, guard };
335
225
  case "quiescing":
336
- this.#transition(row.epoch, "draining", "message claims are fenced; begin bounded drain");
226
+ await this.#transition(row.epoch, "draining", "message claims are fenced; begin bounded drain");
337
227
  return { state: "advanced", phase: "draining", epoch: row.epoch, guard };
338
228
  case "draining": {
339
- writeTransaction((tx) => clearExpiredLegacyMessageClaimsInTx(tx, now));
229
+ await this.#writeStep("clearClaims", { now });
340
230
  const liveClaims = countLiveLegacyMessageClaims(now);
341
231
  await this.#faults.duringClaimDrain?.(row.epoch, liveClaims);
342
232
  if (liveClaims === 0) {
343
- this.#transition(row.epoch, "quiesced", "all live legacy MESSAGE claims drained");
233
+ await this.#transition(row.epoch, "quiesced", "all live legacy MESSAGE claims drained");
344
234
  return { state: "advanced", phase: "quiesced", epoch: row.epoch, liveClaims, guard };
345
235
  }
346
236
  return { state: "holding", phase: "draining", epoch: row.epoch, liveClaims, guard };
@@ -358,11 +248,22 @@ export class TimerMessageCutoverCoordinator {
358
248
  /** Boot recovery is deliberately non-creative: absent/idle shadow state stays a production no-op. */
359
249
  startRecovery(): void {
360
250
  if (this.#ticker) return;
361
- const pass = () => void this.tick().catch((error) => console.warn(`timer MESSAGE cutover recovery failed: ${String(error)}`));
251
+ const pass = framed("timer message-cutover", () => this.tick().catch((error) => console.warn(`timer MESSAGE cutover recovery failed: ${String(error)}`)));
362
252
  pass();
363
253
  this.#ticker = setInterval(pass, this.#tickMs);
364
254
  }
365
255
 
256
+ async #writeStep<K extends keyof CutoverStepArgs>(step: K, args: CutoverStepArgs[K]): Promise<CutoverStepResults[K]> {
257
+ const epoch = this.#epoch;
258
+ const state: CutoverWriterState = { ownerId: this.ownerId, epoch, leaseMs: this.#leaseMs,
259
+ quiesceTimeoutMs: this.#quiesceTimeoutMs, worstCaseMs: this.#worstCaseMs,
260
+ authoritativeActivation: this.#authoritativeActivation, operationLeaseFence: this.#operationLeaseFence };
261
+ const result = await getGateway().write("timerCutover.coordinatorStep", { step, state, args },
262
+ { critical: true, idempotencyKey: randomUUID() }) as unknown as { value: CutoverStepResults[K]; clearEpoch?: boolean };
263
+ if (result.clearEpoch && this.#epoch === epoch) this.#epoch = undefined;
264
+ return result.value;
265
+ }
266
+
366
267
  stop(): void {
367
268
  if (this.#ticker) clearInterval(this.#ticker);
368
269
  this.#ticker = undefined;
@@ -371,57 +272,7 @@ export class TimerMessageCutoverCoordinator {
371
272
  async #acquire(create: boolean): Promise<boolean> {
372
273
  const now = this.#now();
373
274
  let acquiredEpoch: number | undefined;
374
- let takeover = false;
375
- writeTransaction(() => {
376
- // Stage-10 pins the resolved leg before it asks this independent coordinator to close the
377
- // global gate. Check that pin's operation lease under THIS acquisition transaction: if an
378
- // abort won after pin but before requestQuiesce, it cannot be followed by a stale gate close.
379
- const fence = this.#operationLeaseFence;
380
- if (fence) {
381
- const live = getDb().query(`SELECT 1 FROM timer_message_cutover_operation
382
- WHERE id=1 AND request_id=? AND state='running' AND abort_requested=0
383
- AND lease_owner=? AND lease_expires_at > ?`)
384
- .get(fence.requestId, fence.ownerId, now);
385
- if (!live) return;
386
- }
387
- const row = readCutover();
388
- if (!row) {
389
- if (!create) return;
390
- acquiredEpoch = 1;
391
- const journal: TimerMessageCutoverJournalEntry[] = [{ epoch: 1, phase: "leased", at: now, reason: "initial lease acquired" }];
392
- getDb().query(`INSERT INTO timer_message_cutover
393
- (id, protocol_version, epoch, desired_mode, observed_mode, phase, lease_owner, lease_expires_at,
394
- quiesce_started_at, quiesce_deadline, last_completed_timer, ingress_policy,
395
- message_mutation_gate, gate_epoch, abort_requested, phase_journal, updated_at)
396
- VALUES (1, 1, 1, 'shadow', 'shadow', 'leased', ?, ?, NULL, NULL, NULL, 'legacy', 'open', NULL, 0, ?, ?)`)
397
- .run(this.ownerId, now + this.#leaseMs, JSON.stringify(journal), now);
398
- return;
399
- }
400
- const rowPhase = normalizePhase(row.phase);
401
- const liveForeign = row.leaseOwner !== null && row.leaseOwner !== this.ownerId && (row.leaseExpiresAt ?? 0) > now;
402
- if (liveForeign) return;
403
- if (row.leaseOwner === this.ownerId && (row.leaseExpiresAt ?? 0) > now && this.#epoch === row.epoch) {
404
- acquiredEpoch = row.epoch;
405
- return;
406
- }
407
- const nextEpoch = row.epoch + 1;
408
- takeover = row.leaseOwner !== null || rowPhase !== "idle" || row.messageMutationGate === "closed";
409
- const nextPhase: TimerMessageCutoverPhase = rowPhase === "idle" ? "leased" : rowPhase;
410
- const entry: TimerMessageCutoverJournalEntry = {
411
- epoch: nextEpoch,
412
- phase: takeover ? "takeover" : "leased",
413
- at: now,
414
- reason: takeover ? `expired epoch ${row.epoch} taken over in phase ${rowPhase}` : "lease acquired",
415
- };
416
- const result = getDb().query(`UPDATE timer_message_cutover SET
417
- protocol_version = COALESCE(protocol_version, 1), epoch = ?, phase = ?, lease_owner = ?, lease_expires_at = ?,
418
- gate_epoch = CASE WHEN message_mutation_gate = 'closed' THEN ? ELSE NULL END,
419
- phase_journal = ?, updated_at = ?
420
- WHERE id = 1 AND (lease_owner IS NULL OR lease_expires_at <= ? OR lease_owner = ?)`)
421
- .run(nextEpoch, nextPhase, this.ownerId, now + this.#leaseMs, nextEpoch,
422
- appendJournal(row, entry), now, now, this.ownerId);
423
- if (result.changes === 1) acquiredEpoch = nextEpoch;
424
- });
275
+ ({ acquiredEpoch } = await this.#writeStep("acquire", { now, create }));
425
276
  if (acquiredEpoch === undefined) return false;
426
277
  this.#epoch = acquiredEpoch;
427
278
  await this.#faults.afterLeaseAcquire?.(acquiredEpoch);
@@ -430,116 +281,24 @@ export class TimerMessageCutoverCoordinator {
430
281
 
431
282
  async #renew(epoch: number): Promise<boolean> {
432
283
  const now = this.#now();
433
- const renewed = writeTransaction(() => {
434
- const row = readCutover();
435
- if (!row || row.epoch !== epoch || row.leaseOwner !== this.ownerId || (row.leaseExpiresAt ?? 0) <= now) return false;
436
- const deadlineCap = row.messageMutationGate === "closed" && row.quiesceDeadline !== null
437
- ? row.quiesceDeadline
438
- : Number.MAX_SAFE_INTEGER;
439
- const nextExpiry = Math.min(now + this.#leaseMs, deadlineCap);
440
- return getDb().query(`UPDATE timer_message_cutover SET lease_expires_at = ?, phase_journal = ?, updated_at = ?
441
- WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?`)
442
- .run(nextExpiry, appendJournal(row, { epoch, phase: "renewed", at: now }), now,
443
- epoch, this.ownerId, now).changes === 1;
444
- });
284
+ const renewed = await this.#writeStep("renew", { now, epoch });
445
285
  if (renewed) await this.#faults.afterRenewal?.(epoch);
446
286
  return renewed;
447
287
  }
448
288
 
449
- #closeGate(epoch: number, now: number): boolean {
450
- return writeTransaction(() => {
451
- const row = this.#owned(epoch, now);
452
- // `requestQuiesce` acquires the coordinator lease before the following tick closes the gate.
453
- // Recheck the Stage-10 operation lease in this close transaction: an abort (or expiry) that
454
- // won in that interval must leave this independent coordinator open and unleased, never
455
- // permit stale authority to close the MESSAGE gate after the abort's release observed it open.
456
- const fence = this.#operationLeaseFence;
457
- if (fence) {
458
- const live = getDb().query(`SELECT 1 FROM timer_message_cutover_operation
459
- WHERE id=1 AND request_id=? AND state='running' AND abort_requested=0
460
- AND lease_owner=? AND lease_expires_at > ?`)
461
- .get(fence.requestId, fence.ownerId, now);
462
- if (!live) {
463
- const released = getDb().query(`UPDATE timer_message_cutover SET
464
- phase = 'idle', lease_owner = NULL, lease_expires_at = NULL,
465
- quiesce_started_at = NULL, quiesce_deadline = NULL,
466
- message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
467
- desired_mode = observed_mode, phase_journal = ?, updated_at = ?
468
- WHERE id=1 AND epoch=? AND lease_owner=? AND lease_expires_at>? AND phase='leased'`)
469
- .run(appendJournal(row, {
470
- epoch, phase: "idle", at: now,
471
- reason: "Stage-10 operation lease no longer authorizes MESSAGE gate close",
472
- }), now, epoch, this.ownerId, now);
473
- if (released.changes !== 1) throw new Error("cutover lease lost while releasing stale Stage-10 gate request");
474
- this.#epoch = undefined;
475
- return false;
476
- }
477
- }
478
- // Re-read all three deadline sources under the SAME writer lock that closes the gate. A
479
- // deadline mutation that won the race is therefore observed here and refuses entry; one that
480
- // loses cannot commit until after the closed marker and is caught by the next continuous tick.
481
- const guard = this.deadlineGuard(now);
482
- if (!guard.safe) {
483
- const changed = getDb().query(`UPDATE timer_message_cutover SET phase = 'aborting', abort_requested = 1,
484
- phase_journal = ?, updated_at = ? WHERE id = 1 AND epoch = ? AND lease_owner = ? AND phase = 'leased'`)
485
- .run(appendJournal(row, {
486
- epoch, phase: "aborting", at: now,
487
- reason: `deadline guard refused atomic gate close (slack=${guard.slackMs}, required=${guard.requiredMs})`,
488
- }), now, epoch, this.ownerId);
489
- if (changed.changes !== 1) throw new Error("cutover lease lost while refusing gate close");
490
- return false;
491
- }
492
- const deadline = Math.min(
493
- now + this.#quiesceTimeoutMs,
494
- guard.deadlines.earliestDeadline === null ? Number.MAX_SAFE_INTEGER : guard.deadlines.earliestDeadline - 1,
495
- );
496
- const changed = getDb().query(`UPDATE timer_message_cutover SET phase = 'quiescing',
497
- message_mutation_gate = 'closed', gate_epoch = ?, quiesce_started_at = ?, quiesce_deadline = ?,
498
- ingress_policy = CASE WHEN ? THEN 'activating' ELSE ingress_policy END,
499
- phase_journal = ?, updated_at = ?
500
- WHERE id = 1 AND epoch = ? AND lease_owner = ? AND phase = 'leased'`)
501
- .run(epoch, now, deadline, this.#authoritativeActivation ? 1 : 0,
502
- appendJournal(row, { epoch, phase: "quiescing", at: now, reason: "MESSAGE mutation gate closed" }),
503
- now, epoch, this.ownerId);
504
- if (changed.changes !== 1) throw new Error("cutover lease lost before gate close");
505
- return true;
506
- });
289
+ async #closeGate(epoch: number, now: number): Promise<boolean> {
290
+ return await this.#writeStep("closeGate", { epoch, now });
507
291
  }
508
292
 
509
- #transition(epoch: number, phase: TimerMessageCutoverPhase, reason: string): void {
293
+ async #transition(epoch: number, phase: TimerMessageCutoverPhase, reason: string): Promise<void> {
510
294
  const now = this.#now();
511
- writeTransaction(() => {
512
- const row = this.#owned(epoch, now);
513
- const changed = getDb().query(`UPDATE timer_message_cutover SET phase = ?, phase_journal = ?, updated_at = ?
514
- WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?`)
515
- .run(phase, appendJournal(row, { epoch, phase, at: now, reason }), now, epoch, this.ownerId, now);
516
- if (changed.changes !== 1) throw new Error(`cutover lease lost before phase ${phase}`);
517
- });
295
+ await this.#writeStep("transition", { epoch, phase, reason, now });
518
296
  }
519
297
 
520
298
  async #finishAbort(epoch: number, reason: string): Promise<void> {
521
299
  await this.#faults.beforeLeaseRelease?.(epoch);
522
300
  const now = this.#now();
523
- writeTransaction(() => {
524
- const row = this.#owned(epoch, now);
525
- const changed = getDb().query(`UPDATE timer_message_cutover SET
526
- phase = 'idle', lease_owner = NULL, lease_expires_at = NULL,
527
- quiesce_started_at = NULL, quiesce_deadline = NULL,
528
- message_mutation_gate = 'open', gate_epoch = NULL, abort_requested = 0,
529
- phase_journal = ?, updated_at = ?
530
- WHERE id = 1 AND epoch = ? AND lease_owner = ? AND lease_expires_at > ?`)
531
- .run(appendJournal(row, { epoch, phase: "idle", at: now, reason: `gate reopened: ${reason}` }),
532
- now, epoch, this.ownerId, now);
533
- if (changed.changes !== 1) throw new Error("cutover lease lost before abort release");
534
- });
301
+ await this.#writeStep("finishAbort", { epoch, reason, now });
535
302
  this.#epoch = undefined;
536
303
  }
537
-
538
- #owned(epoch: number, now: number): TimerMessageCutoverRow {
539
- const row = readCutover();
540
- if (!row || row.epoch !== epoch || row.leaseOwner !== this.ownerId || (row.leaseExpiresAt ?? 0) <= now) {
541
- throw new Error(`cutover epoch ${epoch} is not owned by ${this.ownerId}`);
542
- }
543
- return row;
544
- }
545
304
  }
@@ -0,0 +1,40 @@
1
+ import { getDb } from "./connection";
2
+ import type { TimerMessageCutoverRow } from "./timer-message-flip";
3
+ import type { TimerMessageCutoverPhase, TimerMessageCutoverJournalEntry } from "./timer-cutover-core";
4
+
5
+ export function normalizePhase(phase: string): TimerMessageCutoverPhase {
6
+ return phase === "leased" || phase === "quiescing" || phase === "draining" || phase === "quiesced" || phase === "aborting"
7
+ ? phase
8
+ : "idle";
9
+ }
10
+
11
+ export function journalOf(row: TimerMessageCutoverRow): TimerMessageCutoverJournalEntry[] {
12
+ return Array.isArray(row.phaseJournal) ? row.phaseJournal as TimerMessageCutoverJournalEntry[] : [];
13
+ }
14
+
15
+ export function appendJournal(row: TimerMessageCutoverRow, entry: TimerMessageCutoverJournalEntry): string {
16
+ return JSON.stringify([...journalOf(row), entry]);
17
+ }
18
+
19
+ export function readCutover(): TimerMessageCutoverRow | null {
20
+ const row = getDb().query("SELECT * FROM timer_message_cutover WHERE id = 1").get() as any;
21
+ return row ? {
22
+ id: 1,
23
+ protocolVersion: row.protocol_version,
24
+ epoch: row.epoch,
25
+ desiredMode: row.desired_mode,
26
+ observedMode: row.observed_mode,
27
+ phase: row.phase,
28
+ leaseOwner: row.lease_owner,
29
+ leaseExpiresAt: row.lease_expires_at,
30
+ quiesceStartedAt: row.quiesce_started_at,
31
+ quiesceDeadline: row.quiesce_deadline,
32
+ lastCompletedTimer: row.last_completed_timer,
33
+ ingressPolicy: row.ingress_policy,
34
+ messageMutationGate: row.message_mutation_gate,
35
+ gateEpoch: row.gate_epoch,
36
+ abortRequested: row.abort_requested === 1,
37
+ phaseJournal: JSON.parse(row.phase_journal),
38
+ updatedAt: row.updated_at,
39
+ } : null;
40
+ }
@@ -1,3 +1,4 @@
1
+ import { readCutover, appendJournal } from "./timer-cutover-state";
1
2
  // Narrow cutover recovery write operations. The release is deliberately critical: it reopens the
2
3
  // dispatch gate after a crashed transfer; shedding it can leave both dispatchers fenced indefinitely.
3
4
 
@@ -19,15 +20,6 @@ interface BatchCheckpoint {
19
20
  done: boolean;
20
21
  }
21
22
 
22
- type TimerMessageCutoverPhase = "idle" | "leased" | "quiescing" | "draining" | "quiesced" | "aborting";
23
-
24
- interface TimerMessageCutoverJournalEntry {
25
- epoch: number;
26
- phase: TimerMessageCutoverPhase | "takeover" | "renewed";
27
- at: number;
28
- reason?: string;
29
- }
30
-
31
23
  function operationRow(): OperationRow | null {
32
24
  return getDb().query("SELECT failures, journal FROM timer_message_cutover_operation WHERE id=1")
33
25
  .get() as OperationRow | null;
@@ -37,37 +29,6 @@ function appendOperationEntry(row: OperationRow, entry: Record<string, unknown>)
37
29
  return JSON.stringify([...JSON.parse(row.journal) as Array<Record<string, unknown>>, entry]);
38
30
  }
39
31
 
40
- function readCutover(): TimerMessageCutoverRow | null {
41
- const row = getDb().query("SELECT * FROM timer_message_cutover WHERE id = 1").get() as any;
42
- return row ? {
43
- id: 1,
44
- protocolVersion: row.protocol_version,
45
- epoch: row.epoch,
46
- desiredMode: row.desired_mode,
47
- observedMode: row.observed_mode,
48
- phase: row.phase,
49
- leaseOwner: row.lease_owner,
50
- leaseExpiresAt: row.lease_expires_at,
51
- quiesceStartedAt: row.quiesce_started_at,
52
- quiesceDeadline: row.quiesce_deadline,
53
- lastCompletedTimer: row.last_completed_timer,
54
- ingressPolicy: row.ingress_policy,
55
- messageMutationGate: row.message_mutation_gate,
56
- gateEpoch: row.gate_epoch,
57
- abortRequested: row.abort_requested === 1,
58
- phaseJournal: JSON.parse(row.phase_journal),
59
- updatedAt: row.updated_at,
60
- } : null;
61
- }
62
-
63
- function journalOf(row: TimerMessageCutoverRow): TimerMessageCutoverJournalEntry[] {
64
- return Array.isArray(row.phaseJournal) ? row.phaseJournal as TimerMessageCutoverJournalEntry[] : [];
65
- }
66
-
67
- function appendJournal(row: TimerMessageCutoverRow, entry: TimerMessageCutoverJournalEntry): string {
68
- return JSON.stringify([...journalOf(row), entry]);
69
- }
70
-
71
32
  /**
72
33
  * Operator-abort escape hatch for an interrupted transfer. This is a writer-op body, so the
73
34
  * gateway executor already owns its transaction; keeping it here avoids a writer-registry →
@@ -1,3 +1,4 @@
1
+ import { retireHandedOffPredecessorBody } from "./token-standing";
1
2
  // Token-db writer-op registry (ADR 0001 §3.1/§3.4/§3.7.1, S3 L-F #1898). Domain-owned map; the op
2
3
  // BODIES live in `../token-db.ts` and the shared `write-ops.ts` edit is one import + spread.
3
4
  //
@@ -72,6 +73,7 @@ function stripHandoffToken(result: unknown): unknown {
72
73
 
73
74
  export const TOKEN_DB_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze({
74
75
  // Non-idempotent (mints a new row each apply); the facade supplies `jti` as the ledger key.
76
+ "tokens.retireHandedOffPredecessor": { offLoop: true, idempotent: true, fn: (args: { jti: string }, ctx) => retireHandedOffPredecessorBody(args.jti, ctx.now) },
75
77
  "tokens.create": {
76
78
  offLoop: true,
77
79
  idempotent: false,
@@ -0,0 +1,12 @@
1
+ import { getGateway } from "./gateway";
2
+ import { trackRelayEvent } from "../relay-event-drain";
3
+
4
+ // A concurrent authentication must see a predecessor as retired while its writer op is pending.
5
+ const retiring = new Set<string>();
6
+ export function tokenRetirementPending(jti: string): boolean { return retiring.has(jti); }
7
+ export function retireHandedOffPredecessor(jti: string): void {
8
+ if (retiring.has(jti)) return;
9
+ retiring.add(jti);
10
+ const write = getGateway().write("tokens.retireHandedOffPredecessor", { jti });
11
+ trackRelayEvent(write.finally(() => retiring.delete(jti)), "token.handoff.retired");
12
+ }
@@ -3,7 +3,7 @@ import { participatesInSpawnDelegation, spawnDelegationState, spawnTreeStopped }
3
3
  import { getDb } from "./connection.ts";
4
4
  import type { TokenRevokeReason } from "../security.ts";
5
5
 
6
- export function isTokenRevoked(jti: string): boolean {
6
+ export function isTokenRevoked(jti: string, retire?: (predecessor: string) => void): boolean {
7
7
  let row: { revoked_at: number | null; retire_jti: string | null; agent_id: string | null } | null;
8
8
  try {
9
9
  // #1245 — the same lookup names a handed-off predecessor this token has not retired yet.
@@ -24,19 +24,16 @@ export function isTokenRevoked(jti: string): boolean {
24
24
  const state = spawnDelegationState(row.agent_id);
25
25
  if (state.participating && (state.displaced || spawnTreeStopped(row.agent_id))) return true;
26
26
  }
27
- if (row.retire_jti) retireHandedOffPredecessor(row.retire_jti);
27
+ if (row.retire_jti) retire?.(row.retire_jti);
28
28
  return false;
29
29
  }
30
30
 
31
31
  // A successor's first authentication proves its holder has it, so the predecessor it replaced (token-db.ts
32
32
  // handoffTokenInternal) is retired as a rotation. A failed write is retried by the successor's next request.
33
- function retireHandedOffPredecessor(jti: string): void {
34
- try {
35
- getDb().query("UPDATE tokens SET revoked_at = COALESCE(revoked_at, ?), revoked_reason = 'rotation' WHERE jti = ? AND (revoked_at IS NULL OR revoked_reason = 'stale')")
36
- .run(Math.floor(Date.now() / 1000), jti);
37
- } catch {
38
- // The successor stays valid; its next authentication retries.
39
- }
33
+ export function retireHandedOffPredecessorBody(jti: string, now: number): null {
34
+ getDb().query("UPDATE tokens SET revoked_at = COALESCE(revoked_at, ?), revoked_reason = 'rotation' WHERE jti = ? AND (revoked_at IS NULL OR revoked_reason = 'stale')")
35
+ .run(Math.floor(now / 1000), jti);
36
+ return null;
40
37
  }
41
38
 
42
39
  // Returns undefined if the token is not revoked, otherwise its reason string
@@ -0,0 +1,33 @@
1
+ import { getDb } from "./connection";
2
+
3
+ export const TOKEN_USAGE_RETENTION_SCHEMA = `
4
+ CREATE TABLE IF NOT EXISTS retired_token_usage_totals (
5
+ agent_id TEXT PRIMARY KEY, tokens INTEGER NOT NULL, unknown_reports INTEGER NOT NULL);
6
+ CREATE TABLE IF NOT EXISTS token_usage_retention_state (
7
+ id INTEGER PRIMARY KEY CHECK (id = 1), expired_before INTEGER NOT NULL);
8
+ `;
9
+
10
+ /** #3619 — old retries must not resurrect reports already folded into permanent budget totals. */
11
+ export function isTokenUsageExpired(createdAt: number): boolean {
12
+ const state = getDb().query("SELECT expired_before FROM token_usage_retention_state WHERE id = 1")
13
+ .get() as { expired_before: number } | null;
14
+ return state !== null && createdAt < state.expired_before;
15
+ }
16
+
17
+ /** Writer-op body: fold each deleted report into its agent's permanent spend in the SAME commit. */
18
+ export function pruneTokenUsage(cutoff: number, limit: number): { pruned: number; moreWork: boolean } {
19
+ const db = getDb();
20
+ db.query(`INSERT INTO token_usage_retention_state VALUES (1, ?)
21
+ ON CONFLICT(id) DO UPDATE SET expired_before = max(expired_before, excluded.expired_before)`).run(cutoff);
22
+ const rows = db.query(`SELECT id, agent_id, input_tokens + output_tokens + cache_read_tokens + cache_write_tokens
23
+ + COALESCE(reasoning_output_tokens, 0) AS tokens, usage_status IS NOT NULL AS unknown_reports
24
+ FROM session_token_usage WHERE created_at < ? ORDER BY created_at, id LIMIT ?`)
25
+ .all(cutoff, limit) as { id: number; agent_id: string; tokens: number; unknown_reports: number }[];
26
+ for (const row of rows) {
27
+ db.query(`INSERT INTO retired_token_usage_totals VALUES (?, ?, ?)
28
+ ON CONFLICT(agent_id) DO UPDATE SET tokens = tokens + excluded.tokens,
29
+ unknown_reports = unknown_reports + excluded.unknown_reports`).run(row.agent_id, row.tokens, row.unknown_reports);
30
+ db.query("DELETE FROM session_token_usage WHERE id = ?").run(row.id);
31
+ }
32
+ return { pruned: rows.length, moreWork: rows.length === limit };
33
+ }
@@ -13,7 +13,7 @@ export const TOKEN_USAGE_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Obj
13
13
  idempotent: true,
14
14
  fn: (args: { input: SessionTokenUsageInput }, ctx: WriterOpContext) => {
15
15
  const result = recordSessionTokenUsage(args.input);
16
- const budgetNotices = recordExhaustedSpawnBudgets(result.record.agentId, ctx.now);
16
+ const budgetNotices = result.record ? recordExhaustedSpawnBudgets(result.record.agentId, ctx.now) : [];
17
17
  return { ...result, budgetNotices };
18
18
  },
19
19
  },