@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
@@ -0,0 +1,121 @@
1
+ // #3762 — warn before a host disk fills. Pure: the single-writer utilization op settles each beat
2
+ // against the stored one (same seam as gate-capacity-alert.ts), so each level change alerts once.
3
+ // Two triggers, one level ladder: used% crossing 85/92/97, or the free-space history projecting
4
+ // full within 24 h (6 h = high). An episode alerts once per worse level and clears with a notice.
5
+ import type { HostDiskWatch, HostMachineSample } from "@humwick/sdk";
6
+
7
+ export type DiskLevel = 1 | 2 | 3;
8
+
9
+ /** Used-share rungs: warn / high / critical. */
10
+ export const DISK_USED_PCT_LEVELS: readonly [number, number, number] = [85, 92, 97];
11
+ /** An episode only clears below this, so a disk hovering on a rung does not flap. */
12
+ export const DISK_CLEAR_USED_PCT = 82;
13
+ export const DISK_ETA_WARN_H = 24;
14
+ export const DISK_ETA_HIGH_H = 6;
15
+ /** Growth needs this much history before it is a rate rather than a blip. */
16
+ export const DISK_MIN_TREND_SPAN_MS = 3 * 3_600_000;
17
+ const HISTORY_SPACING_MS = 30 * 60_000;
18
+ const HISTORY_MAX_SPAN_MS = 25 * 3_600_000;
19
+ const TREND_WINDOW_MS = 24 * 3_600_000;
20
+
21
+ export type DiskAlert =
22
+ | {
23
+ kind: "low"; level: DiskLevel; since: number; freeMb: number; totalMb: number; path?: string;
24
+ /** Free-space decline over the trend window, MB/h; present only while declining. */
25
+ rateMbPerHour?: number; etaHours?: number; spanHours?: number;
26
+ }
27
+ | { kind: "recovered"; since: number; freeMb: number; totalMb: number; path?: string };
28
+
29
+ export interface DiskNotice {
30
+ subject: string;
31
+ body: string;
32
+ priority: "low" | "default" | "high" | "urgent";
33
+ idempotencyKey: string;
34
+ }
35
+
36
+ function usageLevel(usedPct: number): number {
37
+ return DISK_USED_PCT_LEVELS.filter((rung) => usedPct >= rung).length;
38
+ }
39
+
40
+ function growthLevel(etaHours: number | undefined): number {
41
+ if (etaHours === undefined) return 0;
42
+ return etaHours < DISK_ETA_HIGH_H ? 2 : etaHours < DISK_ETA_WARN_H ? 1 : 0;
43
+ }
44
+
45
+ function nextHistory(prev: HostDiskWatch["history"], at: number, freeMb: number): Array<[number, number]> {
46
+ const kept = (prev ?? []).filter(([t]) => t >= at - HISTORY_MAX_SPAN_MS && t < at);
47
+ const last = kept[kept.length - 1];
48
+ if (!last || at - last[0] >= HISTORY_SPACING_MS) kept.push([at, freeMb]);
49
+ return kept;
50
+ }
51
+
52
+ /** Decline rate over the oldest point inside the trend window, or undefined (too little history / not declining). */
53
+ function trend(history: Array<[number, number]>, at: number, freeMb: number): { rateMbPerHour: number; etaHours: number; spanHours: number } | undefined {
54
+ const oldest = history.find(([t]) => t >= at - TREND_WINDOW_MS);
55
+ if (!oldest || at - oldest[0] < DISK_MIN_TREND_SPAN_MS) return undefined;
56
+ const declineMb = oldest[1] - freeMb;
57
+ if (declineMb <= 0) return undefined;
58
+ const spanHours = (at - oldest[0]) / 3_600_000;
59
+ const rateMbPerHour = declineMb / spanHours;
60
+ return { rateMbPerHour, etaHours: freeMb / rateMbPerHour, spanHours };
61
+ }
62
+
63
+ /**
64
+ * Stamps the relay-owned disk watch onto the stored sample and names the one alert owed, if any.
65
+ * A beat with no disk pair keeps the previous watch (missing telemetry is not a state change).
66
+ */
67
+ export function settleDiskWatch(prev: HostMachineSample | undefined, next: HostMachineSample): { machine: HostMachineSample; alert?: DiskAlert } {
68
+ const before = prev?.diskWatch;
69
+ const { diskFreeMb: freeMb, diskTotalMb: totalMb } = next;
70
+ if (freeMb === undefined || totalMb === undefined || totalMb <= 0) {
71
+ return { machine: before ? { ...next, diskWatch: before } : next };
72
+ }
73
+ const history = nextHistory(before?.history, next.at, freeMb);
74
+ const projected = trend(history, next.at, freeMb);
75
+ const usedPct = ((totalMb - freeMb) / totalMb) * 100;
76
+ const level = Math.max(usageLevel(usedPct), growthLevel(projected?.etaHours));
77
+ const prevLevel = before?.level ?? 0;
78
+ const where = next.diskPath ? { path: next.diskPath } : {};
79
+
80
+ if (level > prevLevel) {
81
+ const since = before?.since ?? next.at;
82
+ const watch: HostDiskWatch = { level: level as DiskLevel, since, history };
83
+ return {
84
+ machine: { ...next, diskWatch: watch },
85
+ alert: {
86
+ kind: "low", level: level as DiskLevel, since, freeMb, totalMb, ...where,
87
+ ...(projected ? { rateMbPerHour: projected.rateMbPerHour, etaHours: projected.etaHours, spanHours: projected.spanHours } : {}),
88
+ },
89
+ };
90
+ }
91
+ if (prevLevel > 0 && level === 0 && usedPct < DISK_CLEAR_USED_PCT) {
92
+ return { machine: { ...next, diskWatch: { history } }, alert: { kind: "recovered", since: before!.since ?? next.at, freeMb, totalMb, ...where } };
93
+ }
94
+ return { machine: { ...next, diskWatch: { ...(before?.level ? { level: before.level, since: before.since } : {}), history } } };
95
+ }
96
+
97
+ const gb = (mb: number) => `${(mb / 1024).toFixed(mb >= 10_240 ? 0 : 1)} GB`;
98
+
99
+ export function composeDiskNotice(hostId: string, label: string, alert: DiskAlert): DiskNotice {
100
+ const volume = alert.path ? ` (${alert.path})` : "";
101
+ const usedPct = Math.round(((alert.totalMb - alert.freeMb) / alert.totalMb) * 100);
102
+ if (alert.kind === "recovered") {
103
+ return {
104
+ subject: `Disk recovered on ${label}`,
105
+ body: `${label}${volume} has ${gb(alert.freeMb)} free of ${gb(alert.totalMb)} (${usedPct}% used); the low-disk episode is over. (#3762)`,
106
+ priority: "low",
107
+ idempotencyKey: `host-disk:${hostId}:${alert.since}:recovered`,
108
+ };
109
+ }
110
+ const word = alert.level === 3 ? "CRITICAL" : alert.level === 2 ? "very low" : "low";
111
+ const growth = alert.rateMbPerHour !== undefined && alert.etaHours !== undefined
112
+ ? ` Free space fell ~${gb(alert.rateMbPerHour * 24)}/day over the last ${Math.round(alert.spanHours ?? 0)} h; at that rate it is full in ~${Math.max(1, Math.round(alert.etaHours))} h.`
113
+ : "";
114
+ return {
115
+ subject: `Disk ${word} on ${label}: ${gb(alert.freeMb)} free (${usedPct}% used)`,
116
+ body: `${label}${volume} has ${gb(alert.freeMb)} free of ${gb(alert.totalMb)} (${usedPct}% used).${growth} `
117
+ + "Check workspaces, runtime logs and caches on that host before agents start failing with ENOSPC. (#3762)",
118
+ priority: alert.level === 3 ? "urgent" : alert.level === 2 ? "high" : "default",
119
+ idempotencyKey: `host-disk:${hostId}:${alert.since}:${alert.level}`,
120
+ };
121
+ }
@@ -53,7 +53,8 @@ const DEFAULT_COMPACT_TARGET = 0.3;
53
53
  const DEFAULT_MAX_TASKS_BEFORE_COMPACT = 10;
54
54
  const DEFAULT_COMPACT_COOLDOWN_MS = 60_000;
55
55
 
56
- interface LifecycleManagerOptions { tickMs?: number; now?: () => number; buildManagedSpawnParams?: typeof buildManagedSpawnParamsAsync; }
56
+ type ManagedSpawnParamsBuilder = (...args: Parameters<typeof buildManagedSpawnParamsAsync>) => Record<string, unknown> | Promise<Record<string, unknown>>;
57
+ interface LifecycleManagerOptions { tickMs?: number; now?: () => number; buildManagedSpawnParams?: ManagedSpawnParamsBuilder; }
57
58
 
58
59
  // #1021 A1 — the outcome of trying to wake an on-demand policy via a queued message. A
59
60
  // caller (e.g. wakeRepoSteward) must be able to tell "a live agent will handle this" from
@@ -66,7 +67,7 @@ export class LifecycleManager {
66
67
  private timer?: Timer;
67
68
  private readonly tickMs: number;
68
69
  private readonly now: () => number;
69
- private readonly buildManagedSpawnParams: typeof buildManagedSpawnParamsAsync; private readonly pendingSpawnPreparations = new Map<string, Promise<Command | null>>();
70
+ private readonly buildManagedSpawnParams: ManagedSpawnParamsBuilder; private readonly pendingSpawnPreparations = new Map<string, Promise<Command | null>>();
70
71
  private readonly updateRestartPending = new Set<string>();
71
72
  // #1761 — how many times a policy's stop has been forcefully re-issued while the process refused to
72
73
  // confirm dead. Bounds the escalation so a wedged steward is retried, then given up to `failed`,
@@ -140,7 +140,7 @@ export async function runCdWatcherJob(context: MaintenanceJobContext = { signal:
140
140
  // `offThread: true` mirrors the #1725 off-thread jobs (old-message-prune, db-maintenance, …): it
141
141
  // records in the run's persisted result that the heavy work ran in a subprocess, so an operator
142
142
  // reading the last result sees the same proof the run registry acted on for breaker attribution.
143
- return { ...result, offThread: true, transition: notifyMainCiTransition(result.observation) };
143
+ return { ...result, offThread: true, transition: await notifyMainCiTransition(result.observation) };
144
144
  }
145
145
 
146
146
  /** Best-effort Cosmos deploy hero-shot: read the ledger row this deploy just
@@ -164,9 +164,9 @@ async function emitDeployHeroShot(sha: string | null): Promise<void> {
164
164
  }
165
165
 
166
166
  /** Best-effort transition alert; never throws into the deploy job. */
167
- function notifyMainCiTransition(observation: CiObservation): Record<string, unknown> {
167
+ async function notifyMainCiTransition(observation: CiObservation): Promise<Record<string, unknown>> {
168
168
  try {
169
- const { transition, incidentId, notified, messages } = notifyCiTransition(observation);
169
+ const { transition, incidentId, notified, messages } = await notifyCiTransition(observation);
170
170
  return { flip: transition, notified, messages, ...(incidentId ? { incidentId } : {}) };
171
171
  } catch (error) {
172
172
  const message = errMessage(error);
@@ -17,12 +17,13 @@
17
17
  // - the configured fleet-ops agent target, so something that can actually act is woken.
18
18
 
19
19
  import { errMessage } from "@humwick/sdk";
20
- import { getConfig, setConfig } from "../config-store";
20
+ import { getConfig } from "../config-store";
21
+ import { setConfigAsync } from "../config-store-write";
22
+ import { resetExpiredCriticalRunsAsync } from "../db/maintenance-job-runs-async";
21
23
  import { stewardFallbackTarget } from "../config";
22
24
  import {
23
25
  evaluateCriticalMaintenanceJobs,
24
26
  legacyHungCriticalMaintenanceRuns,
25
- resetExpiredCriticalMaintenanceRuns,
26
27
  type CriticalJobCondition,
27
28
  type LegacyHungCriticalMaintenanceRun,
28
29
  type ResetCriticalMaintenanceRun,
@@ -69,11 +70,11 @@ export interface CriticalJobWatchdogResult {
69
70
  suppressed?: "unchanged-within-reminder-window" | "awaiting-second-failure";
70
71
  }
71
72
 
72
- export function runCriticalJobWatchdog(now: number = Date.now()): CriticalJobWatchdogResult {
73
+ export async function runCriticalJobWatchdog(now: number = Date.now()): Promise<CriticalJobWatchdogResult> {
73
74
  // Do this before evaluating health so the durable scheduler latch is recovered even if the
74
75
  // notification path is unavailable. A normal timed-out run clears itself; this is only the
75
76
  // safety net for a row stranded in `running` after its own deadline lease elapsed.
76
- const resetRuns = resetExpiredCriticalMaintenanceRuns(now);
77
+ const resetRuns = await resetExpiredCriticalRunsAsync(now);
77
78
  const legacyHungRuns = legacyHungCriticalMaintenanceRuns(now);
78
79
  const conditions = evaluateCriticalMaintenanceJobs(now);
79
80
  const errors = conditions.filter((condition) => condition.status === "error");
@@ -86,9 +87,9 @@ export function runCriticalJobWatchdog(now: number = Date.now()): CriticalJobWat
86
87
  state.missedChecks[key] = checks;
87
88
  return checks >= 2;
88
89
  });
89
- const persist = () => {
90
+ const persist = async () => {
90
91
  if (JSON.stringify(state) !== JSON.stringify(prior)) {
91
- setConfig(STATE_NAMESPACE, STATE_KEY, state, "critical-job-watchdog");
92
+ await setConfigAsync(STATE_NAMESPACE, STATE_KEY, state, "critical-job-watchdog");
92
93
  }
93
94
  };
94
95
  const heldRestartEpoch = currentHeldRestartEpoch(now);
@@ -97,7 +98,7 @@ export function runCriticalJobWatchdog(now: number = Date.now()): CriticalJobWat
97
98
  // Recovered (or never degraded): clear the marker so the NEXT degradation notifies immediately
98
99
  // rather than being swallowed by the reminder window opened by the previous one.
99
100
  state.lastNotified = null;
100
- persist();
101
+ await persist();
101
102
  return errors.length > 0
102
103
  ? { status: "error", degradedJobIds: errors.map((condition) => condition.jobId), notified: false, recipients: [], suppressed: "awaiting-second-failure" }
103
104
  : { status: conditions.length > 0 ? "warn" : "ok", degradedJobIds: [], notified: false, recipients: [] };
@@ -115,7 +116,7 @@ export function runCriticalJobWatchdog(now: number = Date.now()): CriticalJobWat
115
116
  ...(heldRestartEpoch ? [`epoch:${heldRestartEpoch.id}:${heldRestartEpoch.blockerCode}`] : []),
116
117
  ].sort().join("|");
117
118
  if (state.lastNotified?.fingerprint === fingerprint && now - state.lastNotified.notifiedAt < DEGRADED_REMINDER_MS) {
118
- persist();
119
+ await persist();
119
120
  return {
120
121
  status: "error", degradedJobIds,
121
122
  ...(resetRuns.length > 0 ? { resetJobIds: resetRuns.map((run) => run.jobId) } : {}),
@@ -154,7 +155,7 @@ export function runCriticalJobWatchdog(now: number = Date.now()): CriticalJobWat
154
155
  }
155
156
 
156
157
  state.lastNotified = { fingerprint, notifiedAt: now };
157
- persist();
158
+ await persist();
158
159
  return {
159
160
  status: "error", degradedJobIds,
160
161
  ...(resetRuns.length > 0 ? { resetJobIds: resetRuns.map((run) => run.jobId) } : {}),
@@ -236,16 +237,14 @@ function composeNotice(
236
237
 
237
238
  /** Handler wrapper: a watchdog that throws must not mark itself failed-and-silent, so failures are
238
239
  * reported as a result the job record carries. */
239
- export function criticalJobWatchdogJob(context: { signal?: AbortSignal } = {}): Record<string, unknown> {
240
- // #1819 — SYNCHRONOUS and single-unit: one `evaluateCriticalMaintenanceJobs` read then one notify
241
- // fan-out that must not be split (the `lastNotified` fingerprint is stamped only after the sends,
242
- // so a half-run would re-page on the next tick). The only safe checkpoint is before it starts, and
243
- // on one thread that is also the only one that could ever fire. Its work is bounded by the number
244
- // of critical jobs (one today), so overrun is not a live risk; if that ever changes this is an
245
- // #1809 offload candidate, not a place for more checks.
240
+ export async function criticalJobWatchdogJob(context: { signal?: AbortSignal } = {}): Promise<Record<string, unknown>> {
241
+ // #1819 — single-unit: one `evaluateCriticalMaintenanceJobs` read then one notify fan-out that must not
242
+ // be split (the `lastNotified` fingerprint is stamped only after the sends, so a half-run would re-page
243
+ // on the next tick). The only safe checkpoint is before it starts. Its two writes (the lease reset and
244
+ // the state persist) are writer ops (#1870 S7).
246
245
  if (context.signal?.aborted) return { status: "ok", aborted: true, degradedJobIds: [], notified: false, recipients: [] };
247
246
  try {
248
- return { ...runCriticalJobWatchdog() };
247
+ return { ...(await runCriticalJobWatchdog()) };
249
248
  } catch (error) {
250
249
  return { status: "error", error: errMessage(error) };
251
250
  }
@@ -1,6 +1,7 @@
1
1
  export {
2
2
  cancelMaintenanceRun,
3
3
  listMaintenanceJobs,
4
+ listMaintenanceJobsAsync,
4
5
  listRecentMaintenanceRuns,
5
6
  listRunningMaintenanceRuns,
6
7
  resetOrphanTrackerForTests,
@@ -498,8 +498,9 @@ export const definitions: MaintenanceJobDefinition[] = [
498
498
  // event loop was stalled, a live replacement's heartbeats may still be unprocessed, so the
499
499
  // grace widens by the observed lag instead of misreading the backlog as post-mortem silence.
500
500
  // #1895 (S3.2 L10, review finding 3) — routed through the async gateway facade.
501
- const revokedOrphanTokens = await revokeOrphanedRuntimeTokensAsync(STALE_TTL_MS, eventLoopLagGraceMs());
502
- return { revokedOrphanTokens };
501
+ // #3776 — paged short writes, bounded by the abort signal and a time budget.
502
+ const { revoked, complete } = await revokeOrphanedRuntimeTokensAsync(STALE_TTL_MS, eventLoopLagGraceMs(), { signal: context.signal });
503
+ return { revokedOrphanTokens: revoked, moreWork: !complete };
503
504
  },
504
505
  },
505
506
  {
@@ -2,8 +2,11 @@ import { timePostBindPhase } from "../post-bind-startup";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { errMessage, isRecord } from "@humwick/sdk";
4
4
  import { classifyPidLiveness } from "@humwick/sdk/process-utils";
5
- import { CRITICAL_MAINTENANCE_PROGRESS_STALE_MS, createActivityEventAsync, getDb, isSqliteBusyError, MAINTENANCE_RUN_AUTORESET_GUARD_SQL, recordDrainConvoy, writeTransaction } from "../db";
5
+ import { CRITICAL_MAINTENANCE_PROGRESS_STALE_MS, createActivityEventAsync, getDb, MAINTENANCE_RUN_AUTORESET_GUARD_SQL, recordDrainConvoy, writeTransaction } from "../db";
6
6
  import { claimMaintenanceRunAsync, finishMaintenanceRunAsync, heartbeatMaintenanceRunAsync } from "../db/maintenance-job-runs-async";
7
+ import { ensureMaintenanceJobsAsync, reclaimDeadLeasesAsync, rescheduleRunOnStartJobsAsync } from "../db/maintenance-job-runs-async";
8
+ import { reclaimDeadLeases, rescheduleRunOnStartJobs, upsertMaintenanceJobs } from "../db/maintenance-job-reconcile";
9
+ import type { DeadLease, MaintenanceJobSpec } from "../db/maintenance-job-reconcile";
7
10
  import type { MaintenanceRunOutcome } from "../db/maintenance-job-runs";
8
11
  import { withAbortDeadline } from "../async-deadline";
9
12
  import { MAINTENANCE_BREAKER_LAG_MS } from "../config";
@@ -173,36 +176,13 @@ export function resetOrphanTrackerForTests(): void {
173
176
  }
174
177
 
175
178
  export function startMaintenanceScheduler(): void {
176
- // Reconcile the job table once at boot. A transient SQLITE_BUSY here must not
177
- // abort startup — the first scheduler tick re-runs the reconcile with retry.
178
- try {
179
- ensureMaintenanceJobs();
180
- scheduleRunOnStartJobs();
181
- } catch (error) {
182
- console.warn(`maintenance job reconcile at startup failed (will retry on next tick): ${errMessage(error)}`);
183
- void ensureMaintenanceJobsResilient()
184
- .then(() => {
185
- scheduleRunOnStartJobs();
186
- return runDueMaintenanceJobs();
187
- })
188
- .catch((retryError) => console.warn(`maintenance startup retry failed (will retry on next tick): ${errMessage(retryError)}`));
189
- }
190
- // #1493 — a relay restart orphans the maintenance-job lease its own dying
191
- // process held moments earlier: lease_owner still names the dead PID and
192
- // lease_until is still in the future, so the claim guard in runJob/
193
- // runDueMaintenanceJobs (lease_until IS NULL OR lease_until <= now) blocks
194
- // the job — e.g. the CD watcher — until the lease's TTL self-expires (up to
195
- // deadlineMs + 5s). Reclaim any lease whose owner PID is no longer alive so
196
- // maintenance resumes immediately after a restart instead of stalling.
197
- try {
198
- reclaimDeadPidLeases();
199
- } catch (error) {
200
- console.warn(`dead-PID maintenance lease reclaim at startup failed (will retry on next tick): ${errMessage(error)}`);
201
- }
179
+ // #1870 S7: the boot reconcile, run-on-start reschedule and dead-lease reclaim are writer ops. The first
180
+ // tick waits for them, so a due run-on-start job is never claimed before its reschedule lands.
181
+ void bootMaintenanceJobs().then(() => runDueMaintenanceJobs())
182
+ .catch((error) => console.warn(`maintenance tick failed: ${errMessage(error)}`));
202
183
  // Floated ticks are wrapped defensively: runDueMaintenanceJobs already catches
203
184
  // its own errors (#1481), but a stray rejection from a floated promise must
204
185
  // never bubble to an unhandledRejection that exits the relay process.
205
- void runDueMaintenanceJobs().catch((error) => console.warn(`maintenance tick failed: ${errMessage(error)}`));
206
186
  if (timer) return;
207
187
  timer = setInterval(
208
188
  () => void runDueMaintenanceJobs().catch((error) => console.warn(`maintenance tick failed: ${errMessage(error)}`)),
@@ -210,6 +190,29 @@ export function startMaintenanceScheduler(): void {
210
190
  );
211
191
  }
212
192
 
193
+ async function bootMaintenanceJobs(): Promise<void> {
194
+ // A transient SQLITE_BUSY here must not abort startup: every scheduler tick runs the reconcile again.
195
+ try {
196
+ await ensureMaintenanceJobsAsync(jobSpecs());
197
+ } catch (error) {
198
+ console.warn(`maintenance job reconcile at startup failed (will retry on next tick): ${errMessage(error)}`);
199
+ }
200
+ try {
201
+ await rescheduleRunOnStartJobsAsync();
202
+ } catch (error) {
203
+ // The next tick retries it, so run-on-start jobs do not wait a full interval (#1870 S7).
204
+ runOnStartPending = true;
205
+ console.warn(`maintenance run-on-start reschedule failed (will retry on next tick): ${errMessage(error)}`);
206
+ }
207
+ // #1493 — a restart orphans the lease its dying process held: lease_owner still names the dead PID and
208
+ // lease_until is still in the future, which blocks the job (e.g. the CD watcher) until the TTL expires.
209
+ try {
210
+ await reclaimDeadLeasesAsync(findDeadLeases());
211
+ } catch (error) {
212
+ console.warn(`dead-PID maintenance lease reclaim at startup failed (will retry on next tick): ${errMessage(error)}`);
213
+ }
214
+ }
215
+
213
216
  /**
214
217
  * `runOnStart` means every Relay process start, not only the first insertion of
215
218
  * a durable maintenance_jobs row. Historically ensureMaintenanceJobs used
@@ -219,29 +222,37 @@ export function startMaintenanceScheduler(): void {
219
222
  * scheduler tick.
220
223
  */
221
224
  export function scheduleRunOnStartJobs(now: number = Date.now()): void {
222
- getDb().query(`
223
- UPDATE maintenance_jobs
224
- SET next_run_at = ?, updated_at = ?
225
- WHERE enabled = 1 AND run_on_start = 1
226
- `).run(now, now);
225
+ rescheduleRunOnStartJobs(now);
227
226
  }
228
227
 
228
+ function readMaintenanceJobs(): MaintenanceJob[] {
229
+ const rows = getDb().query("SELECT * FROM maintenance_jobs ORDER BY title COLLATE NOCASE ASC").all() as MaintenanceJobRow[];
230
+ const now = Date.now();
231
+ const currentIds = new Set(definitions.map((definition) => definition.id));
232
+ return rows.filter((row) => currentIds.has(row.id)).map((row) => rowToJob(row, now));
233
+ }
234
+
235
+ // A transient lock while reconciling must not fail a listing; return the current rows (a later reconcile catches up). #1481.
229
236
  export function listMaintenanceJobs(): MaintenanceJob[] {
230
237
  try {
231
238
  ensureMaintenanceJobs();
232
239
  } catch (error) {
233
- // A transient lock while reconciling must not fail a read-only listing; fall
234
- // through and return the current rows (a later reconcile catches up). #1481.
235
240
  console.warn(`maintenance job reconcile during list failed: ${errMessage(error)}`);
236
241
  }
237
- const rows = getDb().query("SELECT * FROM maintenance_jobs ORDER BY title COLLATE NOCASE ASC").all() as MaintenanceJobRow[];
238
- const now = Date.now();
239
- const currentIds = new Set(definitions.map((definition) => definition.id));
240
- return rows.filter((row) => currentIds.has(row.id)).map((row) => rowToJob(row, now));
242
+ return readMaintenanceJobs();
243
+ }
244
+
245
+ export async function listMaintenanceJobsAsync(): Promise<MaintenanceJob[]> {
246
+ try {
247
+ await ensureMaintenanceJobsAsync(jobSpecs());
248
+ } catch (error) {
249
+ console.warn(`maintenance job reconcile during list failed: ${errMessage(error)}`);
250
+ }
251
+ return readMaintenanceJobs();
241
252
  }
242
253
 
243
254
  export async function runMaintenanceJobNow(id: string, options: { overridePause?: boolean } = {}): Promise<MaintenanceJobRun> {
244
- await ensureMaintenanceJobsResilient();
255
+ await ensureMaintenanceJobsAsync(jobSpecs());
245
256
  const definition = definitions.find((job) => job.id === id);
246
257
  if (!definition) throw new Error("maintenance job not found");
247
258
  return runJob(definition, { force: true, overridePause: options.overridePause });
@@ -266,6 +277,7 @@ async function runMaintenanceJobsNow(ids: string[]): Promise<MaintenanceJobRun[]
266
277
  // no-op (the still-open tick, or the one after, picks up whatever came due). Combined with the
267
278
  // serialized drain below, this is the "one job at a time from a queue" the design (§5.4) calls for.
268
279
  let tickInFlight = false;
280
+ let runOnStartPending = false;
269
281
 
270
282
  export async function runDueMaintenanceJobs(): Promise<void> {
271
283
  if (tickInFlight) return;
@@ -274,7 +286,11 @@ export async function runDueMaintenanceJobs(): Promise<void> {
274
286
  // is guarded: a transient SQLITE_BUSY (or any other failure) reconciling or
275
287
  // querying the job table is logged and the tick is skipped — the next tick retries.
276
288
  try {
277
- await ensureMaintenanceJobsResilient();
289
+ await ensureMaintenanceJobsAsync(jobSpecs());
290
+ if (runOnStartPending) {
291
+ await rescheduleRunOnStartJobsAsync();
292
+ runOnStartPending = false;
293
+ }
278
294
  const now = Date.now();
279
295
  const progressStaleAt = now - CRITICAL_MAINTENANCE_PROGRESS_STALE_MS;
280
296
  const rows = getDb().query(`
@@ -363,84 +379,28 @@ export function drainDueMaintenanceJobsForTests(dueDefinitions: MaintenanceJobDe
363
379
  * otherwise leave it latched, wedging every later test's tick). Production never calls this. */
364
380
  export function resetSchedulerTickGuardForTests(): void {
365
381
  tickInFlight = false;
382
+ runOnStartPending = false;
366
383
  }
367
384
 
368
- // Bounded, ASYNC, loop-yielding retry around the (synchronous) job-table reconcile.
369
- // Mirrors runWriteWithBusyRetry (connection.ts) but wraps ensureMaintenanceJobs,
370
- // which runs its own BEGIN IMMEDIATE transaction and so cannot be nested inside
371
- // runWriteWithBusyRetry. busy_timeout already makes SQLite WAIT on a held lock; the
372
- // rare BUSY that survives it is retried here with jittered backoff that yields the
373
- // event loop between attempts. Anything else — or exhausted attempts — rethrows to
374
- // the caller, which degrades the tick rather than crashing the process (#1481).
375
- const ENSURE_BUSY_RETRY_ATTEMPTS = 4;
376
- const ENSURE_BUSY_RETRY_BASE_MS = 15;
377
- const ENSURE_BUSY_RETRY_MAX_MS = 120;
378
-
379
- async function ensureMaintenanceJobsResilient(): Promise<void> {
380
- for (let attempt = 1; ; attempt++) {
381
- try {
382
- ensureMaintenanceJobs();
383
- return;
384
- } catch (error) {
385
- if (attempt < ENSURE_BUSY_RETRY_ATTEMPTS && isSqliteBusyError(error)) {
386
- const cap = Math.min(ENSURE_BUSY_RETRY_MAX_MS, ENSURE_BUSY_RETRY_BASE_MS * 2 ** (attempt - 1));
387
- const delay = cap / 2 + Math.random() * (cap / 2);
388
- await new Promise((resolve) => setTimeout(resolve, delay));
389
- continue;
390
- }
391
- throw error;
392
- }
393
- }
385
+ function jobSpecs(): MaintenanceJobSpec[] {
386
+ return definitions.map((definition) => ({
387
+ id: definition.id,
388
+ title: definition.title,
389
+ description: definition.description,
390
+ intervalMs: definition.intervalMs,
391
+ deadlineMs: definition.deadlineMs ?? DEFAULT_DEADLINE_MS,
392
+ enabled: !isEphemeralMode() || definition.instanceSafe === true,
393
+ critical: definition.critical === true,
394
+ runOnStart: definition.runOnStart === true,
395
+ }));
394
396
  }
395
397
 
398
+ // Boot/test twin of `ensureMaintenanceJobsAsync`: one BEGIN IMMEDIATE so busy_timeout governs a single lock
399
+ // acquisition (#1481). The listing read path and tests use it; the running scheduler uses the writer op.
396
400
  function ensureMaintenanceJobs(): void {
397
- const db = getDb();
401
+ const specs = jobSpecs();
398
402
  const now = Date.now();
399
- // #1645 — `enabled` is re-asserted from `definitions` on every reconcile, which is what makes it
400
- // CODE-owned and therefore self-healing. Before this it was set only on INSERT and deliberately
401
- // omitted from the conflict clause, because the one column doubled as the operator's "stop" lever
402
- // and a redeploy would have clobbered their intent. Operator intent now lives in
403
- // `maintenance_job_pauses` (holder, reason, expiry), so re-asserting `enabled` overrides nobody —
404
- // and a hand-written `UPDATE maintenance_jobs SET enabled=0` recovers at the next boot instead of
405
- // persisting until someone writes more raw SQL (nine hours of deploy blackout, #1645).
406
- //
407
- // PRECONDITION: convertManualJobDisablesToPauses() (src/db/migrations.ts) must have run first, or
408
- // this silently switches back on a job an operator deliberately stopped.
409
- const stmt = db.query(`
410
- INSERT INTO maintenance_jobs (id, title, description, interval_ms, timeout_ms, enabled, critical, run_on_start, next_run_at, updated_at)
411
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
412
- ON CONFLICT(id) DO UPDATE SET
413
- title = excluded.title,
414
- description = excluded.description,
415
- interval_ms = excluded.interval_ms,
416
- timeout_ms = excluded.timeout_ms,
417
- run_on_start = excluded.run_on_start,
418
- enabled = excluded.enabled, -- #1645: code-owned, see the note above this query
419
- critical = excluded.critical,
420
- next_run_at = COALESCE(maintenance_jobs.next_run_at, excluded.next_run_at),
421
- updated_at = excluded.updated_at
422
- `);
423
- // Batch the upserts into a single BEGIN IMMEDIATE write so busy_timeout governs
424
- // ONE lock acquisition (SQLite waits) instead of one per definition, cutting the
425
- // lock churn that made this loop the crash site under contention (#1481).
426
- writeTransaction(() => {
427
- for (const definition of definitions) {
428
- stmt.run(
429
- definition.id,
430
- definition.title,
431
- definition.description,
432
- definition.intervalMs,
433
- // #1728: the DB column stays `timeout_ms` — renaming it is a migration, not asked for here.
434
- // Code-side this is `deadlineMs`: see MaintenanceJobDefinition for why.
435
- definition.deadlineMs ?? DEFAULT_DEADLINE_MS,
436
- !isEphemeralMode() || definition.instanceSafe === true ? 1 : 0,
437
- definition.critical ? 1 : 0,
438
- definition.runOnStart ? 1 : 0,
439
- definition.runOnStart ? now : now + definition.intervalMs,
440
- now,
441
- );
442
- }
443
- });
403
+ writeTransaction(() => upsertMaintenanceJobs(specs, now));
444
404
  }
445
405
 
446
406
  // #1493 — reclaim current-schema maintenance_jobs leases whose owner PID is proven dead.
@@ -455,23 +415,19 @@ function ensureMaintenanceJobs(): void {
455
415
  // #3645: the crash is not a job failure — it neither bumps consecutive_failures nor replaces an existing
456
416
  // last_error (a non-null one belongs to the live failure streak; success clears it). The page-only guard remains on every
457
417
  // deadline-based reclaim/claim path, where that positive evidence is unavailable.
458
- export function reclaimDeadPidLeases(): void {
459
- const db = getDb();
460
- const now = Date.now();
461
- const leased = db.query(`
462
- SELECT id, lease_owner FROM maintenance_jobs
463
- WHERE lease_owner IS NOT NULL
464
- `).all() as Array<{ id: string; lease_owner: string }>;
418
+ function findDeadLeases(): DeadLease[] {
419
+ const leased = getDb().query("SELECT id, lease_owner FROM maintenance_jobs WHERE lease_owner IS NOT NULL").all() as Array<{ id: string; lease_owner: string }>;
420
+ const dead: DeadLease[] = [];
465
421
  for (const row of leased) {
466
422
  const pid = parseLeaseOwnerPid(row.lease_owner);
467
- if (pid === null || classifyPidLiveness(pid) !== "dead") continue;
468
- db.query(`
469
- UPDATE maintenance_jobs
470
- SET lease_owner = NULL, lease_until = NULL, last_status = 'failed',
471
- last_error = COALESCE(last_error, ?), run_token = NULL, last_progress_at = ?, updated_at = ?
472
- WHERE id = ? AND lease_owner = ?
473
- `).run(`reclaimed stale lease at startup: owner pid ${pid} is confirmed dead`, now, now, row.id, row.lease_owner);
423
+ if (pid !== null && classifyPidLiveness(pid) === "dead") dead.push({ id: row.id, owner: row.lease_owner, pid });
474
424
  }
425
+ return dead;
426
+ }
427
+
428
+ export function reclaimDeadPidLeases(): void {
429
+ const dead = findDeadLeases();
430
+ if (dead.length) writeTransaction(() => reclaimDeadLeases(dead, Date.now()));
475
431
  }
476
432
 
477
433
  function parseLeaseOwnerPid(owner: string): number | null {
@@ -527,7 +483,7 @@ async function runJob(definition: MaintenanceJobDefinition, options: { force?: b
527
483
  const progressStaleAt = startedAt - CRITICAL_MAINTENANCE_PROGRESS_STALE_MS;
528
484
  const claimed = await claimMaintenanceRunAsync({ id: definition.id, owner, runToken, startedAt, leaseUntil, progressStaleAt, force: options.force === true });
529
485
  if (!claimed) {
530
- const state = listMaintenanceJobs().find((job) => job.id === definition.id);
486
+ const state = readMaintenanceJobs().find((job) => job.id === definition.id);
531
487
  return { id: definition.id, status: "skipped", startedAt, finishedAt: Date.now(), durationMs: 0, result: { reason: "not due or already running", state } };
532
488
  }
533
489
 
@@ -1,5 +1,6 @@
1
1
  import { errMessage } from "@humwick/sdk";
2
- import { getConfig, setConfig } from "../config-store";
2
+ import { getConfig } from "../config-store";
3
+ import { setConfigAsync } from "../config-store-write";
3
4
  import { getStalledDeliverySignalAsync } from "../db/stats-async.ts";
4
5
  import { notifyWorkspaceHumanEscalation } from "../workspace-human-escalation";
5
6
  import type { MaintenanceJobDefinition } from "./types";
@@ -56,8 +57,8 @@ export async function runStalledDeliveryPager(now: number = Date.now()): Promise
56
57
  }
57
58
  let alerted = false;
58
59
  let recovered = false;
59
- const persist = () => {
60
- if (JSON.stringify(state) !== JSON.stringify(prior)) setConfig(STATE_NAMESPACE, STATE_KEY, state, "stalled-delivery-pager");
60
+ const persist = async () => {
61
+ if (JSON.stringify(state) !== JSON.stringify(prior)) await setConfigAsync(STATE_NAMESPACE, STATE_KEY, state, "stalled-delivery-pager");
61
62
  };
62
63
 
63
64
  const { check } = signal;
@@ -87,7 +88,7 @@ export async function runStalledDeliveryPager(now: number = Date.now()): Promise
87
88
  if (!state.stalled && state.failStreak >= STALL_PAGE_CONFIRM_CHECKS) state = { ...state, stalled: true, since: now, alerted: false };
88
89
  if (state.stalled && !state.alerted) {
89
90
  // Persist the episode first so a crash after the send re-sends under the same key.
90
- persist();
91
+ await persist();
91
92
  const result = notifyWorkspaceHumanEscalation({
92
93
  subject: "Message delivery stalled",
93
94
  body: [
@@ -107,7 +108,7 @@ export async function runStalledDeliveryPager(now: number = Date.now()): Promise
107
108
  state = { ...state, alerted: true };
108
109
  }
109
110
  }
110
- persist();
111
+ await persist();
111
112
  return { state: state.stalled ? "stalled" : "ok", alerted, recovered };
112
113
  }
113
114
 
@@ -487,7 +487,7 @@ export async function workspaceGC(context: { signal?: AbortSignal } = {}): Promi
487
487
  const proof = landedElsewhere === "confirmed" ? "ancestor" : landedElsewhere;
488
488
  await reconcileLandedWorkspace(fresh, zeroCommitCloseOut
489
489
  ? { strategy: preview && !("available" in preview) ? preview.strategy : "rebase-ff", baseRef, ahead: 0, unmergedAhead: 0, dirtyCount: 0 }
490
- : { strategy: preview && !("available" in preview) ? preview.strategy : "rebase-ff", baseRef, headSha: currentTip, landed: true, landedSha: currentTip });
490
+ : { strategy: preview && !("available" in preview) ? preview.strategy : "rebase-ff", baseRef, headSha: currentTip, landed: true, landedSha: currentTip }, landedElsewhere === "confirmed" ? currentTip : undefined);
491
491
  await patchWorkspaceMetadataAsync(fresh.id, {
492
492
  landedElsewhere: true,
493
493
  landedElsewhereAt: now,