@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
@@ -14,13 +14,26 @@ interface CredentialsDeps {
14
14
  publishRenewalFailure(): void;
15
15
  logRunnerDiagnostic(message: string): void;
16
16
  reportBackgroundRejection(scope: string, status: string, title: string, error: unknown): void;
17
+ /** Recovery gave up: the runner must exit so Relay sees a terminal failure, not a mint loop (#3775). */
18
+ onTerminalAuthFailure(reason: string): void;
17
19
  }
18
20
 
19
21
  const TOKEN_RENEW_LOG_INTERVAL_MS = 5 * 60 * 1000;
20
22
  const TOKEN_RENEW_RETRY_MS = 60_000;
21
- // Debounce reactive token recovery so a burst of 401-ing calls in the same window
22
- // triggers a single re-mint attempt, not one per failing request.
23
- const REACTIVE_TOKEN_RECOVERY_DEBOUNCE_MS = 10_000;
23
+ // #3775 — a successful mint does not prove the hub accepts the runner: an orphaned runner kept
24
+ // getting 401 on its fresh token and re-minted ~2/s. Auth recoveries back off (1 s doubling to
25
+ // 5 min, jittered) and stop after a bounded count; only a quiet spell resets the count.
26
+ export const AUTH_RECOVERY_MAX_ATTEMPTS = 10;
27
+ const AUTH_RECOVERY_BASE_MS = 1_000;
28
+ const AUTH_RECOVERY_MAX_DELAY_MS = 5 * 60_000;
29
+ const AUTH_RECOVERY_QUIET_RESET_MS = 30 * 60_000;
30
+
31
+ /** Delay before auth recovery `attempt` (0-based): the first heals at once, later ones back off with equal jitter. */
32
+ export function authRecoveryDelayMs(attempt: number, random: () => number = Math.random): number {
33
+ if (attempt <= 0) return 0;
34
+ const ceiling = Math.min(AUTH_RECOVERY_MAX_DELAY_MS, AUTH_RECOVERY_BASE_MS * 2 ** (attempt - 1));
35
+ return Math.round(ceiling / 2 + random() * (ceiling / 2));
36
+ }
24
37
 
25
38
  export class Credentials {
26
39
  private currentToken?: string;
@@ -30,7 +43,10 @@ export class Credentials {
30
43
  private tokenRenewTimer?: Timer;
31
44
  private readonly tokenRotation = new TokenRotation();
32
45
  private tokenRenewLastLog?: { key: string; at: number };
33
- private reactiveTokenRecoveryAt?: number;
46
+ private authRecoveryAttempts = 0;
47
+ private authRecoveryLastAt?: number;
48
+ private authRecoveryTimer?: Timer;
49
+ private authRecoveryExhausted = false;
34
50
 
35
51
  constructor(options: { token?: string; tokenJti?: string; tokenProfileId?: string; tokenExpiresAt?: number }, private readonly deps: CredentialsDeps) {
36
52
  this.currentToken = options.token;
@@ -47,6 +63,8 @@ export class Credentials {
47
63
  stop(): void {
48
64
  if (this.tokenRenewTimer) clearTimeout(this.tokenRenewTimer);
49
65
  this.tokenRenewTimer = undefined;
66
+ if (this.authRecoveryTimer) clearTimeout(this.authRecoveryTimer);
67
+ this.authRecoveryTimer = undefined;
50
68
  }
51
69
 
52
70
  // Timer/edge callers cannot await renewal; report an escaping rejection so it cannot
@@ -55,22 +73,35 @@ export class Credentials {
55
73
  void this.renewRuntimeToken().catch((error) => this.deps.reportBackgroundRejection("runtime token renewal", "runtime-token-renewal-failed", "Runtime token renewal aborted", error));
56
74
  }
57
75
 
58
- // The TTL timer cannot detect revocation: recover on auth failure, coalescing a burst.
76
+ // The TTL timer cannot detect revocation: recover on auth failure, coalescing a burst into the
77
+ // one recovery in flight or scheduled.
59
78
  // Orchestrator re-mint heals stale/expired tokens but preserves hard revocation (#484).
60
79
  recoverRuntimeTokenAfterAuthFailure(source: string): void {
61
- if (this.deps.isStopped() || this.tokenRotation.inFlight) return;
80
+ if (this.deps.isStopped() || this.authRecoveryExhausted || this.tokenRotation.inFlight || this.authRecoveryTimer) return;
62
81
  if (!this.isRuntimeTokenRenewable() && !this.canRemintViaOrchestrator()) return;
63
82
  const now = Date.now();
64
- if (this.reactiveTokenRecoveryAt && now - this.reactiveTokenRecoveryAt < REACTIVE_TOKEN_RECOVERY_DEBOUNCE_MS) return;
65
- this.reactiveTokenRecoveryAt = now;
66
- this.deps.logRunnerDiagnostic(`[runner] relay auth failure on ${source}; recovering runtime token`);
67
- this.renewRuntimeTokenInBackground();
83
+ if (this.authRecoveryLastAt !== undefined && now - this.authRecoveryLastAt >= AUTH_RECOVERY_QUIET_RESET_MS) this.authRecoveryAttempts = 0;
84
+ if (this.authRecoveryAttempts >= AUTH_RECOVERY_MAX_ATTEMPTS) {
85
+ this.authRecoveryExhausted = true;
86
+ this.stop();
87
+ this.deps.onTerminalAuthFailure(`runtime-token-auth-failed: relay still rejects the runner (last on ${source}) after ${AUTH_RECOVERY_MAX_ATTEMPTS} token recoveries`);
88
+ return;
89
+ }
90
+ const delayMs = authRecoveryDelayMs(this.authRecoveryAttempts);
91
+ this.authRecoveryAttempts += 1;
92
+ this.authRecoveryLastAt = now;
93
+ this.deps.logRunnerDiagnostic(`[runner] relay auth failure on ${source}; recovering runtime token (attempt ${this.authRecoveryAttempts}/${AUTH_RECOVERY_MAX_ATTEMPTS}${delayMs ? ` in ${Math.ceil(delayMs / 1000)}s` : ""})`);
94
+ if (!delayMs) return this.renewRuntimeTokenInBackground();
95
+ this.authRecoveryTimer = setTimeout(() => {
96
+ this.authRecoveryTimer = undefined;
97
+ this.renewRuntimeTokenInBackground();
98
+ }, delayMs);
68
99
  }
69
100
 
70
101
  scheduleRuntimeTokenRenewal(delayMs?: number): void {
71
102
  if (this.tokenRenewTimer) clearTimeout(this.tokenRenewTimer);
72
103
  this.tokenRenewTimer = undefined;
73
- if (this.deps.isStopped()) return;
104
+ if (this.deps.isStopped() || this.authRecoveryExhausted) return;
74
105
  const canSelfRenew = this.isRuntimeTokenRenewable();
75
106
  const canRemint = this.canRemintViaOrchestrator();
76
107
  // Keep the renewal clock ticking as long as the session can recover its token
@@ -152,7 +183,6 @@ export class Credentials {
152
183
  this.currentTokenProfileId = record.profileId ?? this.currentTokenProfileId;
153
184
  this.currentTokenExpiresAt = record.expiresAt;
154
185
  this.deps.onTokenChanged(token, record);
155
- this.reactiveTokenRecoveryAt = undefined;
156
186
  this.deps.resumeLivenessAndPublishRotation(status, record.jti);
157
187
  this.scheduleRuntimeTokenRenewal();
158
188
  }
@@ -0,0 +1,27 @@
1
+ import type { Message } from "@humwick/sdk";
2
+ import type { HumwickHttpClient } from "@humwick/sdk/http-client";
3
+ import { isNotificationOnlyInput } from "../inbox-carrier";
4
+ import { logger } from "../logger";
5
+
6
+ export async function bootstrapUnreadMessages(http: Pick<HumwickHttpClient, "pollMessages">, agentId: string, enqueue: (message: Message) => void): Promise<void> {
7
+ try {
8
+ const messages = await http.pollMessages({ for: agentId, unread: true, limit: 100, turnDelivery: true });
9
+ for (const message of messages) enqueue(message);
10
+ } catch (error) { logger.error("runner", `inbox bootstrap failed: ${error}`); }
11
+ }
12
+
13
+ /** Keep startup failure visible to each sender, including mail queued before the check (#3751). */
14
+ export async function reportUnavailableMessages(
15
+ http: Pick<HumwickHttpClient, "sendMessage" | "markRead">, agentId: string,
16
+ pending: Map<number, Message>, settled: Set<number>, reason: string,
17
+ ): Promise<void> {
18
+ for (const [id, message] of pending) {
19
+ try {
20
+ if (!isNotificationOnlyInput(message)) await http.sendMessage({ from: agentId, to: message.from, replyTo: id,
21
+ body: reason, idempotencyKey: `provider-startup-${agentId}-${id}`, payload: { kind: "notification" } });
22
+ await http.markRead(id, agentId);
23
+ pending.delete(id);
24
+ settled.add(id);
25
+ } catch (error) { logger.error("runner", `startup failure reply failed: ${error}`); }
26
+ }
27
+ }
@@ -15,7 +15,6 @@ interface LifecycleMilestoneDeps {
15
15
  process(): ManagedProcess | undefined;
16
16
  stopAttachmentCacheSweep(): void;
17
17
  stopTestRunDedupSweep(): void;
18
- stopGateRunLeaseProbe(): void;
19
18
  disposeEmptyTurns(): void;
20
19
  resetEmptyTurns(): void;
21
20
  resetLateClearRecovery(): void;
@@ -92,7 +91,6 @@ export class LifecycleMilestones {
92
91
  private backgroundStopped(): void {
93
92
  this.deps.stopAttachmentCacheSweep();
94
93
  this.deps.stopTestRunDedupSweep();
95
- this.deps.stopGateRunLeaseProbe();
96
94
  // #2389 — before the shutdown await, not after: an escalation or settle timer firing during a
97
95
  // graceful provider shutdown would publish a "produced nothing" verdict about a turn the
98
96
  // shutdown itself ended.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env -S bun --no-env-file
2
+ import { runCli } from "../index";
3
+
4
+ runCli("claude");
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env -S bun --no-env-file
2
+ import { runCli } from "../index";
3
+
4
+ runCli("codex");
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env -S bun --no-env-file
2
+ import { runCli } from "../index";
3
+
4
+ runCli("opencode");
@@ -375,8 +375,9 @@ export function detectApprovalModeFromArgs(provider: string, defaultArgs: string
375
375
  return detector ? detector([...defaultArgs, ...providerArgs]) : undefined;
376
376
  }
377
377
 
378
- if (import.meta.main) {
379
- main().catch((error) => {
378
+ export function runCli(provider?: string): void {
379
+ const argv = provider ? [...process.argv.slice(0, 2), provider, ...process.argv.slice(2)] : process.argv;
380
+ main(argv).catch((error) => {
380
381
  // A fatal startup failure (e.g. the runner could not register with the relay within the
381
382
  // deadline) must exit non-zero, not hang or swallow — the orchestrator's exit detection
382
383
  // then reports it and the spawn-parent is notified instead of waiting on a silent zombie.
@@ -384,3 +385,5 @@ if (import.meta.main) {
384
385
  exitRunner(1, `runner fatal: ${errMessage(error)}`);
385
386
  });
386
387
  }
388
+
389
+ if (import.meta.main) runCli();
@@ -0,0 +1,34 @@
1
+ import { errMessage } from "@humwick/sdk";
2
+ import { checkProviderAuth } from "@humwick/sdk/provider-probe";
3
+ import { selectProviderConfigEnv } from "@humwick/sdk/child-env";
4
+ import type { ProviderAdapter, ProviderStatusEvent } from "./adapter";
5
+ import { logger } from "./logger";
6
+ import { hostSourcesFor } from "./host-asset-plan";
7
+
8
+ /** All real adapters share the managed-spawn login check, including replacements (#3751). */
9
+ export function withProviderSetup(adapter: ProviderAdapter): ProviderAdapter {
10
+ const spawn = adapter.spawn.bind(adapter);
11
+ adapter.spawn = async (config) => {
12
+ const env = selectProviderConfigEnv(config.providerConfig.env, hostSourcesFor(config).settings);
13
+ const auth = await checkProviderAuth(config.provider, undefined, env, config.providerConfig.command);
14
+ if (!auth.ok) throw new Error(auth.reason);
15
+ return spawn(config);
16
+ };
17
+ return adapter;
18
+ }
19
+
20
+ export function providerStartupFailure(error: unknown): ProviderStatusEvent {
21
+ const message = `Provider startup failed: ${errMessage(error)}`;
22
+ return { status: "error", reason: "provider-turn", providerState: {
23
+ state: "blocked", terminal: true, reason: "providerSetupRequired", label: "Provider setup required", message,
24
+ recommendedAction: "Complete the provider login and first-run setup on this host, then restart the runner.",
25
+ } };
26
+ }
27
+
28
+ export async function attemptProviderStartup(start: () => Promise<void>, failed: (status: ProviderStatusEvent) => void): Promise<void> {
29
+ try { await start(); }
30
+ catch (error) {
31
+ logger.error("startup", errMessage(error));
32
+ failed(providerStartupFailure(error));
33
+ }
34
+ }
@@ -1,3 +1,4 @@
1
+ import { withProviderSetup } from "./provider-setup";
1
2
  import { ClaudeApiAdapter } from "./adapters/claude-api";
2
3
  import type { LaunchMode, SpawnProvider } from "@humwick/sdk";
3
4
  import { getAllManifests, supportedLaunchModesForProvider } from "@humwick/providers";
@@ -31,5 +32,5 @@ export function createProviderAdapter(provider: SpawnProvider, providerArgs: str
31
32
  if (launchMode && !supportedLaunchModesForProvider(provider).includes(launchMode)) {
32
33
  throw new Error(`${provider} does not support launchMode=${launchMode}`);
33
34
  }
34
- return registration.createAdapter(providerArgs, launchMode);
35
+ return withProviderSetup(registration.createAdapter(providerArgs, launchMode));
35
36
  }
@@ -1,3 +1,5 @@
1
+ import { attemptProviderStartup } from "./provider-setup";
2
+ import { bootstrapUnreadMessages, reportUnavailableMessages } from "./agent-runner/inbox";
1
3
  import { writeChildEnv } from "@humwick/sdk/env";
2
4
  import { normalizeDeliverResult } from "./delivery-result";
3
5
  import { TokenUsageReportCoverage } from "./agent-runner/token-usage-coverage";
@@ -34,7 +36,6 @@ import { AttachmentCacheSweep } from "./attachment-cache-sweep";
34
36
  import { resolveTestRunRootPid, TEST_RUN_DEDUP_SWEEP_MS, TestRunDedupSweep } from "./test-run-dedup";
35
37
  import { cachedGateRunCommandsForCwd } from "./gate-run-config";
36
38
  import { AreaMemoryTracker } from "./area-memory";
37
- import { GateRunLeaseProbe } from "./gate-run-lease-probe";
38
39
  import { ClaimTracker } from "./claim-tracker";
39
40
  import { ProviderQuotaHarvest } from "./adapters/provider-quota-harvest";
40
41
  import { agentShellLaunchEnv } from "./adapters/project-shell";
@@ -224,7 +225,6 @@ export class AgentRunner {
224
225
  private livenessReconcileTimer?: Timer;
225
226
  private readonly attachmentCacheSweep: AttachmentCacheSweep;
226
227
  private readonly testRunDedupSweep: TestRunDedupSweep;
227
- private readonly gateRunLeaseProbe: GateRunLeaseProbe;
228
228
  private processStartedAt = 0;
229
229
  private providerSessionId = crypto.randomUUID();
230
230
  // Last transcript path seen this session — used by end-of-session Insights (#184)
@@ -363,7 +363,7 @@ export class AgentRunner {
363
363
  });
364
364
  this.lifecycleMilestones = new LifecycleMilestones({
365
365
  options: this.options, agentId: this.agentId, process: () => this.process,
366
- stopAttachmentCacheSweep: () => this.attachmentCacheSweep.stop(), stopTestRunDedupSweep: () => this.testRunDedupSweep.stop(), stopGateRunLeaseProbe: () => this.gateRunLeaseProbe.stop(),
366
+ stopAttachmentCacheSweep: () => this.attachmentCacheSweep.stop(), stopTestRunDedupSweep: () => this.testRunDedupSweep.stop(),
367
367
  disposeEmptyTurns: () => this.emptyTurns.dispose(), resetEmptyTurns: () => this.emptyTurns.reset(), resetLateClearRecovery: () => { this.lateClearRecovery.reset(); this.continuationFence.reset(); },
368
368
  clearLifecycleAction: () => this.lifecycle.setLifecycleAction(undefined), setOffline: () => this.claims.setTerminalStatus("offline"), publishStatus: () => this.publishStatus(),
369
369
  stopDrainTimer: () => { if (this.drainTimer) clearTimeout(this.drainTimer); this.drainTimer = undefined; }, stopSubmitRecoveryPoll: () => this.stopSubmitRecoveryPoll(),
@@ -409,14 +409,12 @@ export class AgentRunner {
409
409
  const sessionProviderPid = () => resolveTestRunRootPid(this.process, (managedProcess) => this.options.adapter.resolveBusyPid?.(managedProcess));
410
410
  // #2461 — ancestry alone made this sweep a no-op for `relay_run` gate runs (a disjoint tree
411
411
  // under the orchestrator). It now also owns anything env-tagged with this agent's id, classifies
412
- // against the repo's CONFIGURED gate commands, and defers to the launch-time lease holder. The
412
+ // against the repo's CONFIGURED gate commands, and reads live leases during each sweep. The
413
413
  // config read is cached for the session — it is a repo file, so re-reading it every 15s for the
414
- // agent's whole life buys nothing. All three helpers live in their own leaves.
415
- this.gateRunLeaseProbe = new GateRunLeaseProbe(this.agentId, TEST_RUN_DEDUP_SWEEP_MS);
414
+ // agent's whole life buys nothing.
416
415
  this.testRunDedupSweep = new TestRunDedupSweep({
417
416
  rootPid: sessionProviderPid, agentId: () => this.agentId,
418
417
  gateCommands: () => cachedGateRunCommandsForCwd(this.options.cwd),
419
- leaseHolderPid: () => this.gateRunLeaseProbe.pid(),
420
418
  protectedPids: () => [process.pid], log: (message) => this.logRunnerDiagnostic(message),
421
419
  });
422
420
  this.context.configureLogger();
@@ -450,6 +448,7 @@ export class AgentRunner {
450
448
  },
451
449
  logRunnerDiagnostic: (message) => this.logRunnerDiagnostic(message),
452
450
  reportBackgroundRejection: (scope, status, title, error) => this.reportBackgroundRejection(scope, status, title, error),
451
+ onTerminalAuthFailure: (reason) => { logger.error("runner", `exiting: ${reason}`); this.options.onProviderExit?.(1, reason); },
453
452
  });
454
453
  const token = this.credentials.current();
455
454
  if (token) publishRuntimeTokenForCli(token, this.credentials.expiresAt, (message) => this.logRunnerDiagnostic(message));
@@ -733,16 +732,17 @@ export class AgentRunner {
733
732
  this.ensureScratch();
734
733
  void sweepRunnerStaleScratch({ listAgents: () => this.http.listAgents(), agentId: this.agentId, cwd: this.options.cwd, fallbackBaseDir: orchestratorBaseDirFromEnv(), log: (message) => this.logRunnerDiagnostic(message) });
735
734
  this.scheduleAttachmentCacheSweep();
736
- this.adoptProcess(await this.spawnProvider());
737
- this.registration.writeRunnerInfoFile();
738
- await this.promptCommands.startLaunchSeededTurn();
739
- this.publishStatus();
740
- await this.promptCommands.deliverInitialPrompt();
735
+ await attemptProviderStartup(async () => {
736
+ this.adoptProcess(await this.spawnProvider());
737
+ this.registration.writeRunnerInfoFile();
738
+ await this.promptCommands.startLaunchSeededTurn();
739
+ this.publishStatus();
740
+ await this.promptCommands.deliverInitialPrompt();
741
+ }, (status) => this.setProviderStatus(status));
741
742
  await this.bootstrapUnreadMessages();
742
743
  this.taskClaims.start();
743
744
  this.startHttpLiveness();
744
745
  this.testRunDedupSweep.start();
745
- this.gateRunLeaseProbe.start();
746
746
  this.credentials.scheduleRuntimeTokenRenewal();
747
747
  this.scheduleDrain();
748
748
  }
@@ -790,6 +790,7 @@ export class AgentRunner {
790
790
  }
791
791
 
792
792
  private handleBusRegistered(): void {
793
+ if (this.process) this.publishStatus();
793
794
  void this.nativeSelfResume.reconcile((id) => this.http.request("GET", `/api/commands/${encodeURIComponent(id)}`)).catch((error) => this.sessionLog(`compact settlement reconcile: ${errMessage(error)}`));
794
795
  // #2973 — a re-register follows a relay restart (a deploy): re-list so changed tools reach the agent.
795
796
  void this.proxy?.refreshTools().catch(() => {}); this.effort.trigger();
@@ -941,14 +942,7 @@ export class AgentRunner {
941
942
  this.scheduleDrain();
942
943
  }
943
944
 
944
- private async bootstrapUnreadMessages(): Promise<void> {
945
- try {
946
- const messages = await this.http.pollMessages({ for: this.agentId, unread: true, limit: 100, turnDelivery: true });
947
- for (const message of messages) this.enqueueMessage(message);
948
- } catch (error) {
949
- logger.error("runner", `inbox bootstrap failed: ${error}`);
950
- }
951
- }
945
+ private bootstrapUnreadMessages = () => bootstrapUnreadMessages(this.http, this.agentId, (message) => this.enqueueMessage(message));
952
946
 
953
947
  private scheduleDrain(delayMs = 0): void {
954
948
  if (this.lifecycle.stopped || this.drainTimer || this.deliverySubmitHold || this.submitResendInFlight) return;
@@ -1363,7 +1357,8 @@ export class AgentRunner {
1363
1357
  private async drainMessages(): Promise<void> {
1364
1358
  if (this.lifecycle.stopped || this.delivering || this.deliverySubmitHold || this.submitResendInFlight || this.pendingMessages.size === 0) return;
1365
1359
  if (!this.process) {
1366
- this.scheduleDrain(500);
1360
+ if (this.terminalFailure) await reportUnavailableMessages(this.http, this.agentId, this.pendingMessages, this.settledMessageIds, this.terminalFailure.message);
1361
+ if (this.pendingMessages.size) this.scheduleDrain(500);
1367
1362
  return;
1368
1363
  }
1369
1364
  this.delivering = true;
@@ -2822,7 +2817,7 @@ export class AgentRunner {
2822
2817
  ?? (activeWork.length === 0 ? this.emptyTurns.providerState() ?? null : null);
2823
2818
  const published = this.publishRunnerStatus({
2824
2819
  agentStatus,
2825
- ready: agentStatus !== "offline" && !this.lifecycle.stopped && !teardownLifecycle && !this.startupPaneDialog && !this.deliverySubmitHold,
2820
+ ready: Boolean(this.process) && agentStatus !== "offline" && !this.lifecycle.stopped && !teardownLifecycle && !this.startupPaneDialog && !this.deliverySubmitHold,
2826
2821
  meta: {
2827
2822
  ...runnerIdentityMeta(this.options, { tmuxSession: providerTerminalSession(this.process), tmuxSocket: providerTerminalSocket(this.process), auth: { profileId: this.credentials.profileId, jti: this.credentials.jti, expiresAt: this.credentials.expiresAt } }, "status"),
2828
2823
  lifecycleAction: this.lifecycle.lifecycleAction ?? null,
@@ -105,6 +105,7 @@ export function parseRunnerCliArgs(
105
105
  else if (arg === "--model-record-provider-model" && ownArgs[i + 1]) modelRecordRaw.providerModel = ownArgs[++i];
106
106
  else if (arg === "--model-record-qualified" && ownArgs[i + 1]) modelRecordRaw.qualifiedModel = ownArgs[++i];
107
107
  else if (arg === "--model-record-effort" && ownArgs[i + 1]) modelRecordRaw.effort = ownArgs[++i];
108
+ else if (arg === "--model-record-efforts" && ownArgs[i + 1]) { try { modelRecordRaw.efforts = JSON.parse(ownArgs[++i]!); } catch {} }
108
109
  else if (arg === "--model-record-window" && ownArgs[i + 1]) {
109
110
  const parsed = Number(ownArgs[++i]);
110
111
  if (Number.isSafeInteger(parsed) && parsed > 0) modelRecordRaw.contextWindowTokens = parsed;
@@ -143,5 +144,5 @@ export function parseRunnerCliArgs(
143
144
  }
144
145
 
145
146
  export function runnerHelp(provider: string): string {
146
- return `${provider}-relay [--headless] [--launch-mode interactive|headless] [--model ALIAS] [--effort LEVEL] [--context-window-tokens N] [--model-record-alias A --model-record-provider-model M --model-record-qualified Q --model-record-effort E --model-record-window N --model-record-max-output N --model-record-threshold JSON] [--rig NAME] [--profile NAME] [--cwd PATH] [--relay-url URL] [--approval MODE] [--label NAME] [--agent-id ID] [--prompt TEXT] [--system-prompt-append TEXT] [--tags a,b] [--caps x,y] [-- provider-args...]`;
147
+ return `${provider}-humwick [--headless] [--launch-mode interactive|headless] [--model ALIAS] [--effort LEVEL] [--context-window-tokens N] [--model-record-alias A --model-record-provider-model M --model-record-qualified Q --model-record-effort E --model-record-window N --model-record-max-output N --model-record-threshold JSON] [--rig NAME] [--profile NAME] [--cwd PATH] [--relay-url URL] [--approval MODE] [--label NAME] [--agent-id ID] [--prompt TEXT] [--system-prompt-append TEXT] [--tags a,b] [--caps x,y] [-- provider-args...]`;
147
148
  }
@@ -1,41 +1,14 @@
1
1
  import { relayEnv } from "@humwick/sdk/env";
2
- // #2142 — per-agent test-run dedup ("latest wins").
3
- //
4
- // Workers run `bun run test:affected` after every file change. When a change touches something
5
- // broad, a single run takes 10-15 minutes — and the agent starts the NEXT run long before the
6
- // previous one finishes. The stacked runs saturate the host: each one holds a gate slot, forks
7
- // its own shard processes, and the oldest run's result is thrown away the moment it lands.
8
- // Instructions don't fix this (an agent rationalizes around them), so the bound has to be
9
- // structural and live BELOW the agent: the runner watches its own session's process tree and,
10
- // whenever it sees more than one independent test run, kills every run except the newest.
11
- //
12
- // Scoping is per-agent BY CONSTRUCTION: the sweep only ever looks at processes it can PROVE belong
13
- // to this agent — descendants of this session's provider process (the tmux pane pid / direct child
14
- // pid the adapter already resolves for busy-liveness), plus processes carrying this agent's
15
- // AGENT_RELAY_ID in their environment. A sibling agent's runs satisfy neither test, so this can
16
- // never fratricide the fleet the way a host-wide `pkill -f` would (#753). It is also
17
- // provider-agnostic — nothing here knows whether the tree belongs to Claude or Codex.
18
- //
19
- // #2461 — the env-tag half of that ownership test is new, and it is why this file exists at all
20
- // after the lease landed. Ancestry ALONE made this a no-op for the case the issue reports: a
21
- // `relay_run` gate run executes under the ORCHESTRATOR daemon, a tree that shares no ancestor with
22
- // the provider pane, so a descendant-scoped sweep could never see it. The orchestrator now stamps
23
- // AGENT_RELAY_ID onto those children, which is what makes them attributable here.
24
- //
25
- // This sweep is now the BACKSTOP, not the primary mechanism. Kill-replace happens at LAUNCH, in
26
- // sdk/src/gate-run-lease.ts, entered from `scripts/gate-slot` and the orchestrator. What is left
27
- // for the sweep is the residual shape neither front door sees: a gate run started straight from the
28
- // provider shell in a repo that does not route through `gate.sh`. It is 15s-late by construction —
29
- // that is the documented coverage gap, tracked for a launch-exact PATH shim in the follow-up issue.
30
- // When a live lease holder exists it is AUTHORITATIVE: the sweep keeps it and kills the rest, so
31
- // the two mechanisms can never disagree about which run survives.
2
+ // #3754 — the lease owns launch-time replacement. The sweep only deduplicates
3
+ // unleased runs with the same captured checkout/revision; ancestry proves ownership,
4
+ // but cannot prove two commands tested the same tree. Live leases protect whole runs.
32
5
  import { execFile } from "node:child_process";
33
6
  import { readFileSync, readdirSync } from "node:fs";
34
7
  import { promisify } from "node:util";
35
8
  import { errMessage } from "@humwick/sdk";
36
9
  import { killPid, waitForPidsExit } from "@humwick/sdk/process-utils";
37
10
  import { classifyGateRun, isTestRunCommand, type GateRunCommands } from "@humwick/sdk/gate-run-classify";
38
- import { GATE_RUN_AGENT_ENV } from "@humwick/sdk/gate-run-lease";
11
+ import { GATE_RUN_AGENT_ENV, GATE_RUN_CONTEXT_ENV, GATE_RUN_HELD_ENV, readLiveGateRunHolderPids } from "@humwick/sdk/gate-run-lease";
39
12
  import type { ManagedProcess } from "./adapter";
40
13
 
41
14
  // Re-exported so the sweep and both lease front doors share ONE definition of "is this a test run".
@@ -215,6 +188,9 @@ export interface TestRunDedupScope {
215
188
  /** Classifier override. Defaults to the argv-shape heuristic; the sweep passes a classifier bound
216
189
  * to the repo's CONFIGURED gate commands so a foreign runner (`pnpm tier:run …`) is matched too. */
217
190
  isGateRun?: (args: string) => boolean;
191
+ /** Launch-time checkout/revision identity. Missing identity cannot prove a duplicate. */
192
+ contextByPid?: ReadonlyMap<number, string>;
193
+ leasedPids?: readonly number[];
218
194
  }
219
195
 
220
196
  export function planTestRunDedup(
@@ -278,29 +254,34 @@ export function planTestRunDedup(
278
254
  }
279
255
  if (runRoots.length < 2) return EMPTY_PLAN;
280
256
 
281
- // Lower elapsed = started later = the run the agent actually wants. `ps etime` only has
282
- // second precision, though, and a Codex agent can launch two commands within that second.
283
- // Pids are allocated in start order within the host pid namespace, so the higher pid is the
284
- // newer root for this otherwise-unresolvable tie. Without this deterministic tie-breaker, two
285
- // same-second delivery runs remain tied forever and #2142 fails to bound the exact burst it
286
- // was added for. The scope is still this session's process tree only.
287
- // A live lease holder wins outright — it is the run the launch-time mechanism already chose. Only
288
- // when there is none (an ungated run in a repo without gate.sh) does newest-wins decide.
289
- const leaseKeep = scope.keepPid !== undefined && runRoots.includes(scope.keepPid) ? scope.keepPid : undefined;
290
- const keptPid = leaseKeep ?? runRoots.reduce((newest, pid) => {
291
- const candidate = byPid.get(pid)!;
292
- const current = byPid.get(newest)!;
293
- return candidate.elapsedSeconds < current.elapsedSeconds
294
- || (candidate.elapsedSeconds === current.elapsedSeconds && pid > newest)
295
- ? pid
296
- : newest;
297
- });
298
- const newestElapsed = byPid.get(keptPid)!.elapsedSeconds;
299
- const staleRoots = runRoots
300
- // With a lease holder the age comparison is meaningless — the holder may be the OLDER run, and
301
- // it still wins. Without one, keep the original guard so a mis-ranked row is never killed.
302
- .filter((pid) => pid !== keptPid && (leaseKeep !== undefined || byPid.get(pid)!.elapsedSeconds >= newestElapsed))
303
- .sort((a, b) => a - b);
257
+ // A lease may belong to a child of the outer shell. Spare the WHOLE run, including
258
+ // ancestors and descendants, rather than signalling its owner indirectly (#3754).
259
+ const leased = new Set(scope.leasedPids ?? []);
260
+ const groups = new Map<string, number[]>();
261
+ const leasedRoots = new Set<number>();
262
+ for (const pid of runRoots) {
263
+ if (subtree(pid).some((child) => leased.has(child))) leasedRoots.add(pid);
264
+ const context = scope.contextByPid ? scope.contextByPid.get(pid) : "legacy";
265
+ if (!context) continue;
266
+ const group = groups.get(context) ?? [];
267
+ group.push(pid);
268
+ groups.set(context, group);
269
+ }
270
+ const staleRoots: number[] = [];
271
+ let keptPid: number | undefined;
272
+ for (const group of groups.values()) {
273
+ if (group.length < 2) continue;
274
+ const leaseKeep = scope.keepPid !== undefined && group.includes(scope.keepPid) ? scope.keepPid : undefined;
275
+ const newest = group.find((pid) => leasedRoots.has(pid)) ?? leaseKeep ?? group.reduce((current, pid) => {
276
+ const candidate = byPid.get(pid)!;
277
+ const prior = byPid.get(current)!;
278
+ return candidate.elapsedSeconds < prior.elapsedSeconds
279
+ || (candidate.elapsedSeconds === prior.elapsedSeconds && pid > current) ? pid : current;
280
+ });
281
+ keptPid = newest;
282
+ staleRoots.push(...group.filter((pid) => pid !== newest && !leasedRoots.has(pid)));
283
+ }
284
+ staleRoots.sort((a, b) => a - b);
304
285
  if (staleRoots.length === 0) return EMPTY_PLAN;
305
286
 
306
287
  const protectedSet = new Set([rootPid, ...protectedPids]);
@@ -334,6 +315,17 @@ export async function readProcessTable(timeoutMs: number = PROCESS_TABLE_TIMEOUT
334
315
  return stdout;
335
316
  }
336
317
 
318
+ /** Only launch-time metadata can distinguish revisions; today's HEAD cannot (#3754). */
319
+ export function readTestRunContext(pid: number): { context?: string; leased?: boolean } {
320
+ try {
321
+ const env = Object.fromEntries(readFileSync(`/proc/${pid}/environ`, "utf8").split("\0").map((entry) => {
322
+ const equals = entry.indexOf("=");
323
+ return [entry.slice(0, equals), entry.slice(equals + 1)];
324
+ }));
325
+ return { context: relayEnv(env, GATE_RUN_CONTEXT_ENV), leased: !!relayEnv(env, GATE_RUN_HELD_ENV) };
326
+ } catch { return {}; }
327
+ }
328
+
337
329
  export interface TestRunDedupDeps {
338
330
  /** Resolve this session's provider pid. undefined ⇒ ancestry proves nothing; the env tag may. */
339
331
  rootPid: () => number | undefined;
@@ -344,6 +336,8 @@ export interface TestRunDedupDeps {
344
336
  gateCommands?: () => GateRunCommands;
345
337
  /** The live lease holder pid, when one exists — authoritative over newest-wins. */
346
338
  leaseHolderPid?: () => number | undefined;
339
+ readRunContext?: (pid: number) => { context?: string; leased?: boolean };
340
+ liveLeasePids?: () => Promise<number[]>;
347
341
  collectOwnedPids?: (agentId: string) => Promise<number[]>;
348
342
  readProcessTable?: () => Promise<string>;
349
343
  kill?: (pid: number, signal: "SIGTERM" | "SIGKILL") => void;
@@ -374,8 +368,17 @@ export async function runTestRunDedupSweep(deps: TestRunDedupDeps): Promise<numb
374
368
  }
375
369
 
376
370
  const gateCommands = deps.gateCommands?.() ?? {};
377
- const plan = planTestRunDedup(parseProcessTable(table), rootPid, deps.protectedPids?.() ?? [], {
378
- ownedPids,
371
+ const rows = parseProcessTable(table);
372
+ const contextByPid = new Map<number, string>();
373
+ const leasedPids = await (deps.liveLeasePids ?? (() => agentId ? readLiveGateRunHolderPids(agentId) : Promise.resolve([])))();
374
+ for (const row of rows) {
375
+ if (classifyGateRun(row.args, gateCommands) === null) continue;
376
+ const context = (deps.readRunContext ?? readTestRunContext)(row.pid);
377
+ if (context.context) contextByPid.set(row.pid, context.context);
378
+ if (context.leased) leasedPids.push(row.pid);
379
+ }
380
+ const plan = planTestRunDedup(rows, rootPid, deps.protectedPids?.() ?? [], {
381
+ ownedPids, contextByPid, leasedPids,
379
382
  keepPid: deps.leaseHolderPid?.(),
380
383
  isGateRun: (args) => classifyGateRun(args, gateCommands) !== null,
381
384
  });
@@ -27,6 +27,19 @@ export function resolveCanonicalRelayDbPath(env: NodeJS.ProcessEnv = process.env
27
27
  return resolve(env.DB_PATH || join(defaultStateDir(), "agent-relay.db"));
28
28
  }
29
29
 
30
+ /** #3301 — the deploy ledger lives only on the hub. Off the hub a release publishes and stops. */
31
+ export function hasCanonicalDeployLedger(env: NodeJS.ProcessEnv = process.env): boolean {
32
+ return existsSync(resolveCanonicalRelayDbPath(env));
33
+ }
34
+
35
+ function openCanonicalDeployLedger(): void {
36
+ const path = resolveCanonicalRelayDbPath();
37
+ if (!existsSync(path)) {
38
+ throw new Error(`no deploy ledger at ${path}: deploys run on the hub. CD deploys a published release there, or run \`bun run release --deploy-only\` on the hub.`);
39
+ }
40
+ openExistingDb(path);
41
+ }
42
+
30
43
  /**
31
44
  * #1763 — the git repository cd-watcher's detached child `git fetch`es against every
32
45
  * tick. cd-watcher provisions and owns a DEDICATED clone here, deliberately separate
@@ -146,7 +159,7 @@ export function withDeployLedgerDb<T>(dbPath: string, fn: () => T): T {
146
159
 
147
160
  /** Claim the one live-ledger lease; both the watcher and release.ts use this path. */
148
161
  export function claimCanonicalDeployLease(input: { commitSha: string; version: string; hosts: DeployHost[]; initiatedBy: string; attachedEventId?: string; previousSha?: string }): DeployEvent | null {
149
- openExistingDb(resolveCanonicalRelayDbPath());
162
+ openCanonicalDeployLedger();
150
163
  const attached = input.attachedEventId ? getDeployEvent(input.attachedEventId) : undefined;
151
164
  if (attached && (attached.status !== "pending" || attached.commitSha !== input.commitSha)) {
152
165
  throw new Error(`deploy event ${input.attachedEventId} is not a pending lease for ${input.commitSha}`);
@@ -159,6 +172,14 @@ export function claimCanonicalDeployLease(input: { commitSha: string; version: s
159
172
  return tryBeginDeployEvent(input);
160
173
  }
161
174
 
175
+ /** A scoped release step borrows its parent's lease, including when deploying a rollback SHA (#3470). */
176
+ export function attachCutoverDeployLease(id: string): DeployEvent {
177
+ openCanonicalDeployLedger();
178
+ const event = getDeployEvent(id);
179
+ if (!event || event.status !== "pending" || !event.initiatedBy.startsWith("humwick-cutover:")) throw new Error(`Not a pending cutover lease: ${id}`);
180
+ return event;
181
+ }
182
+
162
183
  /**
163
184
  * #2105 — stamp THIS driver process onto the lease it just claimed, so cd-watcher's reconcile can
164
185
  * reclaim a stranded row on executor liveness (minutes) instead of the 2h lease TTL. Best-effort by
@@ -193,7 +214,7 @@ export function startDeployLeaseHeartbeat(id: string): ReturnType<typeof setInte
193
214
 
194
215
  /** Explicit emergency recovery; normal crash recovery remains the bounded TTL. */
195
216
  export function clearCanonicalDeployLease(id: string): boolean {
196
- openExistingDb(resolveCanonicalRelayDbPath());
217
+ openCanonicalDeployLedger();
197
218
  return !!forceFailDeployEvent({ id, reason: "force-cleared by operator via release.ts" });
198
219
  }
199
220
 
@@ -206,7 +227,7 @@ export function clearCanonicalDeployLease(id: string): boolean {
206
227
  * the clock, is the authority here).
207
228
  */
208
229
  export function markCanonicalDeploySucceeded(id: string): boolean {
209
- openExistingDb(resolveCanonicalRelayDbPath());
230
+ openCanonicalDeployLedger();
210
231
  return !!reconcilePendingDeployEvent({
211
232
  id, status: "succeeded", requireLeaseExpired: false,
212
233
  reason: "marked succeeded by operator via release.ts (#1532)",