@humwick/server 0.130.1 → 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 (437) hide show
  1. package/README.md +5 -0
  2. package/docs/guides/tools-administration.md +4 -0
  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-B_rFEbnL.js → fleet-C7jQWbw2.js} +2 -2
  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 +8 -0
  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 +3 -3
  128. package/src/config-store-write.ts +13 -1
  129. package/src/config-store.ts +25 -14
  130. package/src/db/activity-async.ts +6 -1
  131. package/src/db/activity-write-ops.ts +12 -1
  132. package/src/db/activity.ts +1 -14
  133. package/src/db/agent-search.ts +1 -15
  134. package/src/db/agents-async.ts +52 -5
  135. package/src/db/agents-write-ops.ts +12 -3
  136. package/src/db/agents.ts +81 -54
  137. package/src/db/ci-incidents-async.ts +5 -5
  138. package/src/db/ci-incidents-write-ops.ts +2 -2
  139. package/src/db/command-row.ts +10 -2
  140. package/src/db/commands-list-query.ts +2 -0
  141. package/src/db/commands-spawn-read.ts +1 -1
  142. package/src/db/config-store-write-ops.ts +7 -2
  143. package/src/db/continuation-archives-async.ts +5 -0
  144. package/src/db/continuation-archives-write-ops.ts +7 -0
  145. package/src/db/continuations.ts +2 -2
  146. package/src/db/db-writer-worker.ts +8 -1
  147. package/src/db/deploy-events-async.ts +1 -0
  148. package/src/db/deploy-events-read-ops.ts +2 -1
  149. package/src/db/deploy-events.ts +35 -4
  150. package/src/db/hosts-registration.ts +10 -2
  151. package/src/db/hosts.ts +9 -6
  152. package/src/db/inbox.ts +0 -14
  153. package/src/db/index.ts +2 -0
  154. package/src/db/integrations-async.ts +8 -0
  155. package/src/db/integrations-ops.ts +15 -0
  156. package/src/db/integrations.ts +15 -21
  157. package/src/db/issue-lifecycle.ts +3 -4
  158. package/src/db/issue-refs-async.ts +20 -0
  159. package/src/db/issue-refs-write-ops.ts +29 -0
  160. package/src/db/issue-refs.ts +7 -8
  161. package/src/db/long-running-work.ts +2 -2
  162. package/src/db/main-connection-writes.ts +23 -2
  163. package/src/db/maintenance-job-reconcile.ts +61 -0
  164. package/src/db/maintenance-job-runs-async.ts +14 -0
  165. package/src/db/maintenance-job-write-ops.ts +9 -0
  166. package/src/db/mappers.ts +2 -14
  167. package/src/db/merge-lease.ts +1 -15
  168. package/src/db/migrations.ts +7 -15
  169. package/src/db/orchestrators.ts +4 -72
  170. package/src/db/pairs.ts +1 -13
  171. package/src/db/project-profile-instructions.ts +50 -0
  172. package/src/db/prompt-versions.ts +9 -2
  173. package/src/db/provider-config-history.ts +8 -3
  174. package/src/db/provider-model-seed.ts +34 -0
  175. package/src/db/read-op-marshaling-fixtures.ts +5 -0
  176. package/src/db/read-ops.ts +4 -0
  177. package/src/db/routing-policy.ts +2 -3
  178. package/src/db/schema.ts +9 -3
  179. package/src/db/spawn-delegation-state.ts +11 -4
  180. package/src/db/stats-async.ts +4 -3
  181. package/src/db/task-entity.ts +23 -2
  182. package/src/db/task-list.ts +3 -1
  183. package/src/db/tasks.ts +1 -17
  184. package/src/db/timer-ingress.ts +7 -1
  185. package/src/db/token-db-write-ops.ts +17 -0
  186. package/src/db/token-standing.ts +15 -11
  187. package/src/db/workflow-contact.ts +1 -1
  188. package/src/db/workflow-task-read-ops.ts +30 -0
  189. package/src/db/write-ops.ts +6 -0
  190. package/src/db/writer-gateway.ts +17 -11
  191. package/src/dispatch-availability.ts +10 -6
  192. package/src/ephemeral/{landing-ingress-proof-worker.ts → landing-ingress-proof.test-fixture.ts} +4 -2
  193. package/src/host/disk-alert.ts +121 -0
  194. package/src/lifecycle-manager.ts +3 -2
  195. package/src/maintenance/cd-watcher.ts +3 -3
  196. package/src/maintenance/critical-job-watchdog.ts +16 -17
  197. package/src/maintenance/index.ts +1 -0
  198. package/src/maintenance/jobs.ts +3 -2
  199. package/src/maintenance/scheduler.ts +84 -128
  200. package/src/maintenance/stalled-delivery-pager.ts +6 -5
  201. package/src/maintenance/workspaces.ts +1 -1
  202. package/src/managed-policy.ts +31 -27
  203. package/src/mcp/index.ts +5 -2
  204. package/src/mcp/output-descriptions.ts +8 -8
  205. package/src/mcp/presentation.ts +4 -4
  206. package/src/mcp/project-profile-instructions.ts +32 -0
  207. package/src/mcp/tool-groups.ts +1 -1
  208. package/src/mcp/tools-spawn.ts +4 -3
  209. package/src/mcp-agent-action.ts +1 -2
  210. package/src/mcp-issue-ref-tools.ts +12 -11
  211. package/src/mcp-maintenance-tools.ts +2 -2
  212. package/src/mcp-profile-trust-tools.ts +4 -4
  213. package/src/mcp-provider-catalog.ts +2 -2
  214. package/src/mcp-task-tools.ts +11 -5
  215. package/src/mcp-team-template-tools.ts +3 -3
  216. package/src/mcp-workflow-tools.ts +10 -3
  217. package/src/notification-types.ts +2 -1
  218. package/src/operator-alarm.ts +26 -11
  219. package/src/project-instructions.ts +4 -0
  220. package/src/project-profile-instruction-text.ts +6 -0
  221. package/src/project-profile-instructions.ts +20 -0
  222. package/src/provider-catalog-store.ts +3 -49
  223. package/src/provider-config-store.ts +12 -11
  224. package/src/provider-model-seed.ts +1 -18
  225. package/src/provider-quota-config-store.ts +5 -4
  226. package/src/provider-secrets.test-fixture.ts +3 -3
  227. package/src/recipe-runner.ts +2 -2
  228. package/src/repo-config-source.ts +10 -4
  229. package/src/routes/agent-profiles.ts +6 -6
  230. package/src/routes/agents-spawn.ts +1 -1
  231. package/src/routes/agents.ts +6 -6
  232. package/src/routes/commands-listing.ts +4 -0
  233. package/src/routes/commands.ts +2 -2
  234. package/src/routes/continuation-archives.ts +2 -2
  235. package/src/routes/index.ts +10 -1
  236. package/src/routes/integrations.ts +24 -50
  237. package/src/routes/issue-refs.ts +7 -7
  238. package/src/routes/orchestrator-bootstrap.ts +1 -1
  239. package/src/routes/orchestrator-host-registration.ts +2 -2
  240. package/src/routes/orchestrator-host-utilization.ts +3 -1
  241. package/src/routes/orchestrator-proxy.ts +2 -2
  242. package/src/routes/orchestrator.ts +2 -2
  243. package/src/routes/project-profile-instructions.ts +27 -0
  244. package/src/routes/project-trust.ts +2 -2
  245. package/src/routes/provider-config.ts +6 -6
  246. package/src/routes/provider-quota-config.ts +2 -2
  247. package/src/routes/settings.ts +5 -5
  248. package/src/routes/stats.ts +6 -6
  249. package/src/routes/task-http.ts +15 -1
  250. package/src/routes/task-issues.ts +11 -26
  251. package/src/routes/tasks.ts +22 -53
  252. package/src/routes/team-templates.ts +5 -5
  253. package/src/routes/teams-config.ts +2 -2
  254. package/src/routes/tokens.ts +12 -5
  255. package/src/routes/workflows.ts +20 -5
  256. package/src/routing-policy-normalize.ts +1 -2
  257. package/src/routing-step-defaults.ts +0 -2
  258. package/src/runner-token-mint-limit.ts +63 -0
  259. package/src/runtime-grant.ts +4 -2
  260. package/src/runtime-tokens.ts +39 -7
  261. package/src/security.ts +13 -20
  262. package/src/server-background.ts +1 -1
  263. package/src/server.ts +5 -2
  264. package/src/services/host-utilization.ts +23 -0
  265. package/src/services/idle-epics.ts +5 -6
  266. package/src/services/issue-lifecycle.ts +57 -61
  267. package/src/services/issue-refs.ts +9 -13
  268. package/src/services/kernel-timers-cutover-chaos-worker.ts +10 -7
  269. package/src/services/kernel-timers-ingress.ts +1 -1
  270. package/src/services/landed-ancestry.ts +19 -3
  271. package/src/services/memory-areas.ts +15 -51
  272. package/src/services/plan-ci-notice.ts +57 -0
  273. package/src/services/plan-ci-recovery.ts +2 -55
  274. package/src/services/plan-wake-digest.ts +1 -0
  275. package/src/services/profile-trust.ts +4 -3
  276. package/src/services/project-config-read.ts +70 -0
  277. package/src/services/project-profile-instructions.ts +54 -0
  278. package/src/services/provisioning-harness/budget-baseline.json +4 -4
  279. package/src/services/restart-spawn.ts +61 -49
  280. package/src/services/self-care-state.ts +2 -1
  281. package/src/services/self-effort.ts +5 -5
  282. package/src/services/send-message.ts +5 -1
  283. package/src/services/spawn-agent.ts +13 -2
  284. package/src/services/spawn-chain-inspector.ts +6 -13
  285. package/src/services/spawn-policy.ts +68 -0
  286. package/src/services/spawn-tree-cleanup.ts +2 -2
  287. package/src/services/task-dispatch.ts +3 -0
  288. package/src/services/task-state-reconcile.ts +6 -14
  289. package/src/services/team-templates.ts +9 -8
  290. package/src/services/transient-agent-reaper.ts +3 -1
  291. package/src/services/workflow-auto-start.ts +25 -13
  292. package/src/services/workflow-ci-main.ts +1 -1
  293. package/src/services/workflow-deferred-cleanup.ts +4 -2
  294. package/src/services/workflow-effect-driver.ts +2 -0
  295. package/src/services/workflow-effects.ts +18 -11
  296. package/src/services/workflow-land-notice.ts +10 -0
  297. package/src/services/workflow-pause-text.ts +43 -0
  298. package/src/services/workflow-reconcile.ts +13 -11
  299. package/src/services/workflow-run-view.ts +2 -0
  300. package/src/services/workflow-runs.ts +29 -13
  301. package/src/services/workflow-supervisor-state.ts +5 -0
  302. package/src/services/workflow-supervisor.ts +7 -3
  303. package/src/settings/registry.ts +1 -2
  304. package/src/spawn-command.ts +4 -4
  305. package/src/spawn-models.ts +10 -12
  306. package/src/spawn-placement.ts +1 -1
  307. package/src/spawn-targets.ts +1 -1
  308. package/src/startup-context-injection.ts +1 -0
  309. package/src/state-adoption/cd-cutover.ts +53 -7
  310. package/src/state-adoption/fleet-cutover.ts +35 -0
  311. package/src/state-adoption/fleet-plan.ts +36 -0
  312. package/src/state-adoption/index.ts +2 -2
  313. package/src/state-adoption/linux-plan.ts +2 -2
  314. package/src/state-adoption/linux.ts +2 -2
  315. package/src/state-adoption/macos-plan.ts +2 -2
  316. package/src/state-adoption/macos.ts +1 -1
  317. package/src/steward.ts +7 -6
  318. package/src/task-dispatch-live.ts +1 -1
  319. package/src/task-dispatcher.ts +1 -1
  320. package/src/teams-config-store.ts +4 -3
  321. package/src/token-db-write.ts +10 -0
  322. package/src/workspace-command-settle.ts +2 -1
  323. package/src/workspace-steward-wake.ts +2 -2
  324. package/node_modules/@humwick/kernel/LICENSE +0 -167
  325. package/node_modules/@humwick/kernel/async-entity-persistence.ts +0 -344
  326. package/node_modules/@humwick/kernel/async-event-log.ts +0 -107
  327. package/node_modules/@humwick/kernel/automations.pilot.ts +0 -147
  328. package/node_modules/@humwick/kernel/config.ts +0 -111
  329. package/node_modules/@humwick/kernel/contract.ts +0 -572
  330. package/node_modules/@humwick/kernel/control-authz.ts +0 -281
  331. package/node_modules/@humwick/kernel/control.ts +0 -645
  332. package/node_modules/@humwick/kernel/effects.ts +0 -493
  333. package/node_modules/@humwick/kernel/event-log.ts +0 -319
  334. package/node_modules/@humwick/kernel/http-transport.ts +0 -384
  335. package/node_modules/@humwick/kernel/identity-grammar.ts +0 -20
  336. package/node_modules/@humwick/kernel/identity.ts +0 -248
  337. package/node_modules/@humwick/kernel/index.ts +0 -28
  338. package/node_modules/@humwick/kernel/kernel-persistence.ts +0 -181
  339. package/node_modules/@humwick/kernel/landing-incident.ts +0 -306
  340. package/node_modules/@humwick/kernel/landing.ts +0 -953
  341. package/node_modules/@humwick/kernel/manifest-validation.ts +0 -460
  342. package/node_modules/@humwick/kernel/mcp-transport.ts +0 -109
  343. package/node_modules/@humwick/kernel/message-authority-chaos-worker.ts +0 -200
  344. package/node_modules/@humwick/kernel/message-authority.ts +0 -281
  345. package/node_modules/@humwick/kernel/module-host.ts +0 -722
  346. package/node_modules/@humwick/kernel/openapi-init.ts +0 -14
  347. package/node_modules/@humwick/kernel/package.json +0 -39
  348. package/node_modules/@humwick/kernel/persistence-schema.ts +0 -248
  349. package/node_modules/@humwick/kernel/persistence.ts +0 -510
  350. package/node_modules/@humwick/kernel/registry.ts +0 -60
  351. package/node_modules/@humwick/kernel/sqlite-kernel-persistence.ts +0 -499
  352. package/node_modules/@humwick/kernel/sqlite-module-host.ts +0 -87
  353. package/node_modules/@humwick/kernel/timers-chaos-worker.ts +0 -186
  354. package/node_modules/@humwick/kernel/timers.pilot.ts +0 -1411
  355. package/node_modules/@humwick/kernel/workflow-activation.ts +0 -25
  356. package/node_modules/@humwick/kernel/workflow-backstop.ts +0 -40
  357. package/node_modules/@humwick/kernel/workflow-cancel-tree.ts +0 -20
  358. package/node_modules/@humwick/kernel/workflow-chaos-worker.ts +0 -213
  359. package/node_modules/@humwick/kernel/workflow-composition.ts +0 -231
  360. package/node_modules/@humwick/kernel/workflow-container.ts +0 -72
  361. package/node_modules/@humwick/kernel/workflow-control.test-fixture.ts +0 -42
  362. package/node_modules/@humwick/kernel/workflow-control.ts +0 -178
  363. package/node_modules/@humwick/kernel/workflow-definition.ts +0 -507
  364. package/node_modules/@humwick/kernel/workflow-dispatch.ts +0 -60
  365. package/node_modules/@humwick/kernel/workflow-foreach.ts +0 -167
  366. package/node_modules/@humwick/kernel/workflow-journal-codec.ts +0 -339
  367. package/node_modules/@humwick/kernel/workflow-journal.ts +0 -922
  368. package/node_modules/@humwick/kernel/workflow-lane.ts +0 -48
  369. package/node_modules/@humwick/kernel/workflow-lease-owner.ts +0 -60
  370. package/node_modules/@humwick/kernel/workflow-recovery.test-fixture.ts +0 -19
  371. package/node_modules/@humwick/kernel/workflow-runtime.ts +0 -938
  372. package/node_modules/@humwick/kernel/workflow-script.ts +0 -11
  373. package/node_modules/@humwick/kernel/workflow-state.ts +0 -299
  374. package/node_modules/@humwick/kernel/workflow-step-commit.ts +0 -195
  375. package/node_modules/@humwick/kernel/workflow-transitions.ts +0 -317
  376. package/node_modules/@humwick/kernel/workflow-values.ts +0 -153
  377. package/node_modules/@humwick/kernel/workflow.pilot.ts +0 -376
  378. package/node_modules/@humwick/offload/LICENSE +0 -167
  379. package/node_modules/@humwick/offload/README.md +0 -57
  380. package/node_modules/@humwick/offload/dist/async-runner.d.ts +0 -18
  381. package/node_modules/@humwick/offload/dist/async-runner.d.ts.map +0 -1
  382. package/node_modules/@humwick/offload/dist/async-runner.js +0 -158
  383. package/node_modules/@humwick/offload/dist/async-runner.js.map +0 -1
  384. package/node_modules/@humwick/offload/dist/envelope.d.ts +0 -25
  385. package/node_modules/@humwick/offload/dist/envelope.d.ts.map +0 -1
  386. package/node_modules/@humwick/offload/dist/envelope.js +0 -80
  387. package/node_modules/@humwick/offload/dist/envelope.js.map +0 -1
  388. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts +0 -10
  389. package/node_modules/@humwick/offload/dist/fixtures/worker-task.d.ts.map +0 -1
  390. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js +0 -59
  391. package/node_modules/@humwick/offload/dist/fixtures/worker-task.js.map +0 -1
  392. package/node_modules/@humwick/offload/dist/index.d.ts +0 -5
  393. package/node_modules/@humwick/offload/dist/index.d.ts.map +0 -1
  394. package/node_modules/@humwick/offload/dist/index.js +0 -5
  395. package/node_modules/@humwick/offload/dist/index.js.map +0 -1
  396. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts +0 -62
  397. package/node_modules/@humwick/offload/dist/subprocess-runner.d.ts.map +0 -1
  398. package/node_modules/@humwick/offload/dist/subprocess-runner.js +0 -295
  399. package/node_modules/@humwick/offload/dist/subprocess-runner.js.map +0 -1
  400. package/node_modules/@humwick/offload/dist/types.d.ts +0 -111
  401. package/node_modules/@humwick/offload/dist/types.d.ts.map +0 -1
  402. package/node_modules/@humwick/offload/dist/types.js +0 -176
  403. package/node_modules/@humwick/offload/dist/types.js.map +0 -1
  404. package/node_modules/@humwick/offload/dist/worker-pool.d.ts +0 -23
  405. package/node_modules/@humwick/offload/dist/worker-pool.d.ts.map +0 -1
  406. package/node_modules/@humwick/offload/dist/worker-pool.js +0 -435
  407. package/node_modules/@humwick/offload/dist/worker-pool.js.map +0 -1
  408. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts +0 -2
  409. package/node_modules/@humwick/offload/dist/worker-runtime.d.ts.map +0 -1
  410. package/node_modules/@humwick/offload/dist/worker-runtime.js +0 -27
  411. package/node_modules/@humwick/offload/dist/worker-runtime.js.map +0 -1
  412. package/node_modules/@humwick/offload/dist/worker.d.ts +0 -5
  413. package/node_modules/@humwick/offload/dist/worker.d.ts.map +0 -1
  414. package/node_modules/@humwick/offload/dist/worker.js +0 -1
  415. package/node_modules/@humwick/offload/dist/worker.js.map +0 -1
  416. package/node_modules/@humwick/offload/package.json +0 -42
  417. package/node_modules/@humwick/offload/src/async-runner.ts +0 -170
  418. package/node_modules/@humwick/offload/src/envelope.ts +0 -92
  419. package/node_modules/@humwick/offload/src/fixtures/worker-task.ts +0 -49
  420. package/node_modules/@humwick/offload/src/index.ts +0 -4
  421. package/node_modules/@humwick/offload/src/subprocess-runner.ts +0 -334
  422. package/node_modules/@humwick/offload/src/types.ts +0 -235
  423. package/node_modules/@humwick/offload/src/worker-pool.ts +0 -463
  424. package/node_modules/@humwick/offload/src/worker-runtime.ts +0 -32
  425. package/node_modules/@humwick/offload/src/worker.ts +0 -5
  426. package/public/assets/agent-liveness-DyqxfB-t.js +0 -2
  427. package/public/assets/agent-profiles-BpI_jTxq.js +0 -2
  428. package/public/assets/index-B7ZerHLY.css +0 -2
  429. package/public/assets/index-BU3uWo2t.js +0 -15
  430. package/public/assets/projects-B_lmcqfO.js +0 -2
  431. package/public/assets/provider-display-DbLWpP3j.js +0 -2
  432. package/public/assets/run-card-CO_bguQf.js +0 -2
  433. package/public/assets/use-project-list-B-T2Bnnn.js +0 -2
  434. package/public/assets/versioned-prompt-editor-Diu8BIaM.js +0 -8
  435. package/runner/src/gate-run-lease-probe.ts +0 -38
  436. package/src/db/claude-catalog-reconcile.ts +0 -27
  437. package/src/routing-model-aliases.ts +0 -21
@@ -104,13 +104,18 @@ function rowToIntent(row: IntentDbRow): TimerIngressIntentRow {
104
104
  };
105
105
  }
106
106
 
107
+ const installedOn = new WeakSet<object>();
108
+
107
109
  /** Idempotent, crash-safe schema install (one transaction; re-running over existing data is a
108
110
  * no-op). Bootstrap DDL — it runs ONCE at startup as its own committed statement, never lazily
109
111
  * inside an op, so a rolled-back op can never undo the schema it ran against (§4 allowlist). */
110
112
  export function installTimerIngressSchema(
111
- options: { requestKeyMigrationFaults?: TimerIngressRequestKeyMigrationFaults } = {},
113
+ options: { requestKeyMigrationFaults?: TimerIngressRequestKeyMigrationFaults; ifNotInstalled?: boolean } = {},
112
114
  ): void {
113
115
  const db = getDb();
116
+ // #1870 S7: `startServer` installs it before bind (the boot exception); `createTimerIngress` passes
117
+ // `ifNotInstalled` so its later call runs no DDL on the main connection. Explicit calls always install.
118
+ if (options.ifNotInstalled && installedOn.has(db)) return;
114
119
  db.transaction(() => {
115
120
  // AUTOINCREMENT is deliberate: `seq` must be monotone and NEVER reused (the watermark's meaning
116
121
  // depends on it — and it doubles as the per-timer apply FENCE), even if settled rows are pruned.
@@ -165,6 +170,7 @@ export function installTimerIngressSchema(
165
170
  db.run("INSERT OR IGNORE INTO timer_ingress_watermark (id, applied_seq, updated_at) VALUES (1, 0, 0)");
166
171
  options.requestKeyMigrationFaults?.beforeCommit?.();
167
172
  }).immediate();
173
+ installedOn.add(db);
168
174
  }
169
175
 
170
176
  // ── Reads ───────────────────────────────────────────────────────────────────────────────────────
@@ -25,6 +25,7 @@ import {
25
25
  type PruneExpiredTokenRecordsInput,
26
26
  } from "../token-db.ts";
27
27
  import type { TokenRecord } from "../types.ts";
28
+ import { recordLaunchFact, type LaunchFact } from "./launch-facts.ts";
28
29
  import type { TokenRevokeReason } from "../security.ts";
29
30
  import type { WriterOpContext, WriterOpSpec } from "./write-ops.ts";
30
31
 
@@ -41,6 +42,11 @@ export interface HandoffTokenOpArgs {
41
42
  readonly newJti: string;
42
43
  }
43
44
 
45
+ /** Args for `tokens.createRunner`: a runner mint plus, for a fresh launch, its launch fact (#1870 S7). */
46
+ export interface CreateRunnerTokenOpArgs extends CreateTokenOpArgs {
47
+ readonly launch?: { readonly agentId: string; readonly fact: LaunchFact };
48
+ }
49
+
44
50
  export interface RevokeTokenOpArgs {
45
51
  readonly jti: string;
46
52
  readonly reason?: TokenRevokeReason;
@@ -72,6 +78,17 @@ export const TOKEN_DB_WRITE_OPS: Readonly<Record<string, WriterOpSpec>> = Object
72
78
  fn: (args: CreateTokenOpArgs, ctx: WriterOpContext) => createTokenInternal(args.input, args.jti, ctx.now),
73
79
  serializeIdempotencyResult: stripCreateToken,
74
80
  },
81
+ // A runner mint and the launch fact its successors derive from, in one transaction.
82
+ "tokens.createRunner": {
83
+ offLoop: true,
84
+ idempotent: false,
85
+ fn: (args: CreateRunnerTokenOpArgs, ctx: WriterOpContext) => {
86
+ const issued = createTokenInternal(args.input, args.jti, ctx.now);
87
+ if (args.launch) recordLaunchFact(args.launch.agentId, args.launch.fact, ctx.now);
88
+ return issued;
89
+ },
90
+ serializeIdempotencyResult: stripCreateToken,
91
+ },
75
92
  // Non-idempotent (mints and links a successor); folds the eligibility read (ADR §3.4).
76
93
  "tokens.handoff": {
77
94
  offLoop: true,
@@ -61,24 +61,28 @@ export function tokenRevocationReason(jti: string): string | null | undefined {
61
61
  /** A rotation retires one token for its successor, so it is never a lineage revocation. */
62
62
  export type LineageRevokeReason = Exclude<TokenRevokeReason, "rotation">;
63
63
 
64
- /** #1245 — a token and every token handed off from it (token-db.ts handoffTokenInternal) are one credential, so
65
- * revoking any of them revokes the whole lineage: unretired predecessors, pending successors and their renewals.
66
- * A hard reason also hardens stale members, so no auth, renewal or orchestrator recovery reaches the lineage. A
67
- * stale walk stops at stale members: what follows them is an orchestrator recovery. It never enters a `keep` token. */
68
- export function revokeTokenLineage(seeds: readonly string[], reason: LineageRevokeReason | null, nowSeconds: number, keep: readonly string[] = []): string[] {
69
- if (seeds.length === 0) return [];
70
- const rows = getDb().query(`
64
+ // #3776 — each step must be a key lookup whatever sqlite_stat1 says: CROSS JOIN pins the join order and the unary
65
+ // `+` keeps the predecessor index off the backward step, so stale stats cannot make the walk scan per member.
66
+ export const TOKEN_LINEAGE_REVOKE_SQL = `
71
67
  WITH RECURSIVE kept(jti) AS (SELECT value FROM json_each(?1)),
72
68
  lineage(jti) AS (
73
69
  SELECT value FROM json_each(?2) WHERE value NOT IN kept
74
- UNION SELECT t.jti FROM lineage l JOIN tokens p ON p.jti = l.jti JOIN tokens t ON t.predecessor_jti = p.jti
70
+ UNION SELECT t.jti FROM lineage l CROSS JOIN tokens p ON p.jti = l.jti CROSS JOIN tokens t ON t.predecessor_jti = p.jti
75
71
  WHERE t.jti NOT IN kept AND (?4 IS NOT 'stale' OR p.revoked_reason IS NOT 'stale')
76
- UNION SELECT p.predecessor_jti FROM lineage l JOIN tokens p ON p.jti = l.jti
77
- WHERE p.predecessor_jti IS NOT NULL AND p.predecessor_jti NOT IN kept AND (?4 IS NOT 'stale' OR p.revoked_reason IS NOT 'stale')
72
+ UNION SELECT p.predecessor_jti FROM lineage l CROSS JOIN tokens p ON p.jti = l.jti
73
+ WHERE +p.predecessor_jti IS NOT NULL AND +p.predecessor_jti NOT IN kept AND (?4 IS NOT 'stale' OR p.revoked_reason IS NOT 'stale')
78
74
  )
79
75
  UPDATE tokens SET revoked_at = COALESCE(revoked_at, ?3), revoked_reason = ?4
80
76
  WHERE jti IN lineage AND (revoked_at IS NULL OR (?4 IS NOT 'stale' AND revoked_reason = 'stale'))
81
77
  RETURNING jti
82
- `).all(JSON.stringify(keep), JSON.stringify(seeds), nowSeconds, reason) as Array<{ jti: string }>;
78
+ `;
79
+
80
+ /** #1245 — a token and every token handed off from it (token-db.ts handoffTokenInternal) are one credential, so
81
+ * revoking any of them revokes the whole lineage: unretired predecessors, pending successors and their renewals.
82
+ * A hard reason also hardens stale members, so no auth, renewal or orchestrator recovery reaches the lineage. A
83
+ * stale walk stops at stale members: what follows them is an orchestrator recovery. It never enters a `keep` token. */
84
+ export function revokeTokenLineage(seeds: readonly string[], reason: LineageRevokeReason | null, nowSeconds: number, keep: readonly string[] = []): string[] {
85
+ if (seeds.length === 0) return [];
86
+ const rows = getDb().query(TOKEN_LINEAGE_REVOKE_SQL).all(JSON.stringify(keep), JSON.stringify(seeds), nowSeconds, reason) as Array<{ jti: string }>;
83
87
  return rows.map((row) => row.jti);
84
88
  }
@@ -20,7 +20,7 @@ export interface WorkflowHeldContact { runId: string; reason: WorkflowContactRea
20
20
 
21
21
  /** Notices that bypass the step-contact hold: the run's own coordinator notices (including GO awaiting an external
22
22
  * hold's release), and a step agent held on a provider block, which waits until someone acts (#3647). */
23
- const COORDINATOR_NOTICE_KINDS = new Set(["workflow.started", "workflow.landed", "workflow.escalated", "workflow.result", "workflow.review-passed", "agent.spawn_failed", "agent.spawn_recovered", "delegation.budget_exhausted", "agent.provider_blocked", "agent.provider_still_held"]);
23
+ const COORDINATOR_NOTICE_KINDS = new Set(["workflow.started", "workflow.landed", "workflow.escalated", "workflow.result", "workflow.review-passed", "workflow.paused", "workflow.unpaused", "agent.spawn_failed", "agent.spawn_recovered", "delegation.budget_exhausted", "agent.provider_blocked", "agent.provider_still_held"]);
24
24
  export const WORKFLOW_SCOPE = "local";
25
25
  export const WORKFLOW_RUN_TABLE = "kernel_entity__workflow__run";
26
26
  const LINK_TABLE = "kernel_entity__workflow__link";
@@ -185,3 +185,33 @@ export function openWorkflowRunHolders(): { teamIds: Set<string>; coordinatorIds
185
185
  }
186
186
  return holders;
187
187
  }
188
+
189
+ /** #3757 — a running run a coordinator paused, with its input Task's team; `pause` is the stored JSON. */
190
+ export interface PausedWorkflowRunRow {
191
+ runId: string; taskId: number; taskTitle: string | null; coordinator: string; callerAgentId: string | null;
192
+ teamId: string | null; owner: string | null; pause: string;
193
+ }
194
+
195
+ let pauseColumnOn: unknown;
196
+ /** The column appears when the workflow module first registers after #3757; until then nothing is paused. */
197
+ function pauseColumnReady(): boolean {
198
+ const db = getDb();
199
+ if (pauseColumnOn === db) return true;
200
+ const ready = (db.query(`PRAGMA table_info(${WORKFLOW_RUN_TABLE})`).all() as Array<{ name: string }>).some((column) => column.name === "pause");
201
+ if (ready) pauseColumnOn = db;
202
+ return ready;
203
+ }
204
+
205
+ export function pausedWorkflowRuns(): PausedWorkflowRunRow[] {
206
+ if (!workflowTablesReady() || !pauseColumnReady()) return [];
207
+ return getDb().query(`SELECT run.id AS runId, run.taskId AS taskId, task.title AS taskTitle, run.coordinator AS coordinator, run.callerAgentId AS callerAgentId,
208
+ task.team_id AS teamId, task.owner AS owner, run.pause AS pause
209
+ FROM ${WORKFLOW_RUN_TABLE} run LEFT JOIN tasks task ON task.id = run.taskId
210
+ WHERE run.scope = ? AND run.status = 'running' AND run.pause IS NOT NULL ORDER BY run.id`).all(WORKFLOW_SCOPE) as PausedWorkflowRunRow[];
211
+ }
212
+
213
+ /** #3757 — a paused run keeps its idle step agents: the idle reapers leave them alone until it is unpaused. */
214
+ export function workflowRunPaused(runId: string): boolean {
215
+ if (!workflowTablesReady() || !pauseColumnReady()) return false;
216
+ return Boolean(getDb().query(`SELECT 1 FROM ${WORKFLOW_RUN_TABLE} WHERE scope = ? AND id = ? AND status = 'running' AND pause IS NOT NULL`).get(WORKFLOW_SCOPE, runId));
217
+ }
@@ -1,3 +1,4 @@
1
+ import { INTEGRATIONS_WRITE_OPS } from "./integrations-ops.ts";
1
2
  import { bufferedLastSeen, type PresenceSnapshot } from "./presence-buffer";
2
3
  import { ensureDurablePayloadSchema, readRetryReceipt, storeDurablePayload } from "./durable-payloads";
3
4
  import { ledgerSlotPredicate, ledgerSlots, WRITER_IDEMPOTENCY_RETENTION_MS } from "./writer-ledger";
@@ -5,6 +6,7 @@ import { workspaceChangeListeners } from "./workspaces";
5
6
  import { withWriterOpState } from "./main-thread-state";
6
7
  import { importVaultRows } from "./secrets-vault-import";
7
8
  import { PROMPT_VERSION_WRITE_OPS } from "./prompt-versions-ops";
9
+ import { PROJECT_PROFILE_INSTRUCTION_WRITE_OPS } from "./project-profile-instructions";
8
10
  import { VAULT_WRITE_OPS } from "./secrets-vault-write";
9
11
  import { VAULT_EXPIRY_WRITE_OPS } from "./vault-expiry";
10
12
  import { VAULT_BACKUP_WRITE_OPS } from "./vault-backup";
@@ -117,6 +119,7 @@ import { RESTART_EPOCH_WRITE_OPS } from "./restart-epoch-write-ops.ts";
117
119
  import { TOKEN_USAGE_WRITE_OPS } from "./token-usage-write-ops.ts";
118
120
  // #1870 S3 — the 1 s raw writers: maintenance leases/runs, pending issue closes, workspace lifecycle.
119
121
  import { MAINTENANCE_JOB_WRITE_OPS } from "./maintenance-job-write-ops.ts";
122
+ import { ISSUE_REFS_WRITE_OPS } from "./issue-refs-write-ops.ts";
120
123
  import { LONG_RUNNING_WORK_WRITE_OPS } from "./long-running-work-write-ops.ts";
121
124
  import { WORKSPACE_WRITE_OPS } from "./workspace-write-ops.ts";
122
125
 
@@ -406,6 +409,7 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
406
409
  ...ROUTING_POLICY_WRITE_OPS,
407
410
  ...USER_SETTINGS_WRITE_OPS,
408
411
  ...PROMPT_VERSION_WRITE_OPS,
412
+ ...PROJECT_PROFILE_INSTRUCTION_WRITE_OPS,
409
413
  ...TOKEN_DB_WRITE_OPS,
410
414
  ...DELIVERY_WRITE_OPS,
411
415
  ...TIMER_WRITE_OPS,
@@ -440,6 +444,7 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
440
444
  ...SPAWN_TREE_CLEANUP_WRITE_OPS,
441
445
  ...PROJECTS_WRITE_OPS,
442
446
  ...CHANNELS_WRITE_OPS,
447
+ ...INTEGRATIONS_WRITE_OPS,
443
448
  ...CI_INCIDENTS_WRITE_OPS,
444
449
  ...TEAMS_WRITE_OPS,
445
450
  ...TEAM_SPAWN_DELEGATION_WRITE_OPS,
@@ -468,6 +473,7 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
468
473
  ...TOKEN_USAGE_WRITE_OPS,
469
474
  ...MAINTENANCE_JOB_WRITE_OPS,
470
475
  ...LONG_RUNNING_WORK_WRITE_OPS,
476
+ ...ISSUE_REFS_WRITE_OPS,
471
477
  ...WORKSPACE_WRITE_OPS,
472
478
  ...WEB_TOOL_CALL_WRITE_OPS,
473
479
  ...CLAUDE_API_BUDGET_WRITE_OPS,
@@ -174,8 +174,9 @@ export interface WriterStats {
174
174
  * SQLite lock (H4 §3.6.1). A wedged writer is NOT idle — this is deliberately distinct from
175
175
  * `busy:0`, which is how the naive pool-replace path mis-reported the stall. New non-critical
176
176
  * writes shed and critical writes are held until the wedged op completes and the FIFO resumes.
177
+ * #3776 — names the op holding the writer and how long it has run, so health says WHICH job; null when not wedged.
177
178
  */
178
- wedged: boolean;
179
+ wedged: { op: string; runningMs: number } | null;
179
180
  /**
180
181
  * #2276 — how many times the IN-PROCESS drain has yielded the event loop mid-convoy because its
181
182
  * time slice was spent. 0 on the worker backend (nothing to slice). A steadily climbing count is
@@ -259,7 +260,7 @@ let drainYields = 0;
259
260
  // lock (§3.6.1). Cleared when that in-flight job finally settles (the op completed and released the
260
261
  // lock), which re-kicks the FIFO drain. Worker backend only — an in-process op runs synchronously on
261
262
  // the main thread, so it cannot be observed mid-stall.
262
- let wedged = false;
263
+ let wedgedOp: { op: string; startedAt: number } | undefined;
263
264
 
264
265
  // Test-only fault injection for the precise H5 window: the op has COMMITTED, but its
265
266
  // acknowledgement is lost before the caller observes the result. One-shot by design so a retry
@@ -355,14 +356,14 @@ function createFileWriter(dbPath: string): WorkerBackend {
355
356
 
356
357
  /** Server shutdown keeps already-admitted writes; test/reset teardown remains immediate. */
357
358
  export async function drainWriterOnShutdown(): Promise<void> {
358
- try { await shutdownDrain.start(() => !draining && !wedged && pendingWriteDepth() === 0 && inFlightWrites() === 0); }
359
+ try { await shutdownDrain.start(() => !draining && !wedgedOp && pendingWriteDepth() === 0 && inFlightWrites() === 0); }
359
360
  finally { await shutdownWriter(); }
360
361
  }
361
362
 
362
363
  export async function shutdownWriter(): Promise<void> {
363
364
  const current = backend;
364
365
  backend = undefined;
365
- wedged = false;
366
+ wedgedOp = undefined;
366
367
  // Reject anything still queued at the gateway so no caller hangs across a teardown.
367
368
  const stranded = submissionQueue.splice(0, submissionQueue.length);
368
369
  for (const entry of stranded) {
@@ -379,7 +380,8 @@ export function isWriterInitialized(): boolean {
379
380
  }
380
381
 
381
382
  export function writerStats(): WriterStats {
382
- if (!backend) return { backend: "uninitialized", queued: 0, busy: 0, highWater: queueHighWater, wedged: false, drainYields };
383
+ if (!backend) return { backend: "uninitialized", queued: 0, busy: 0, highWater: queueHighWater, wedged: null, drainYields };
384
+ const wedged = wedgedOp ? { op: wedgedOp.op, runningMs: Math.round(performance.now() - wedgedOp.startedAt) } : null;
383
385
  if (backend.kind === "in-process") {
384
386
  return { backend: "in-process", queued: pendingWriteDepth(), busy: inFlightWrites(), highWater: queueHighWater, wedged, drainYields };
385
387
  }
@@ -480,7 +482,7 @@ function admitOrThrow(op: string, opts: WriteOpts | undefined, count: number): v
480
482
  const occupancy = pendingWriteDepth() + inFlightWrites();
481
483
  if (occupancy + count > queueHardCap) throw new WriterOverloadedError(op, occupancy);
482
484
  if (opts?.critical) return;
483
- const workerWedged = wedged;
485
+ const workerWedged = wedgedOp !== undefined;
484
486
  if (workerWedged || occupancy + count > queueHighWater) throw new WriterOverloadedError(op, occupancy);
485
487
  }
486
488
 
@@ -511,7 +513,7 @@ async function drainLoop(): Promise<void> {
511
513
  // second concurrent drain and anything submitted meanwhile is picked up by this same loop when
512
514
  // it resumes — FIFO is preserved, the queue is only PAUSED.
513
515
  let sliceStartedAt = performance.now();
514
- while (submissionQueue.length > 0 && !wedged) {
516
+ while (submissionQueue.length > 0 && !wedgedOp) {
515
517
  const entry = submissionQueue.shift()!;
516
518
  const onLoop = backend?.kind === "in-process";
517
519
  await dispatchEntry(entry);
@@ -521,7 +523,7 @@ async function drainLoop(): Promise<void> {
521
523
  sliceStartedAt = performance.now();
522
524
  continue;
523
525
  }
524
- if (submissionQueue.length === 0 || wedged) continue;
526
+ if (submissionQueue.length === 0 || wedgedOp) continue;
525
527
  if (performance.now() - sliceStartedAt < drainSliceMs) continue;
526
528
  drainYields += 1;
527
529
  await yieldEventLoop();
@@ -529,7 +531,7 @@ async function drainLoop(): Promise<void> {
529
531
  }
530
532
  } finally {
531
533
  draining = false;
532
- shutdownDrain.notify(!wedged && pendingWriteDepth() === 0 && inFlightWrites() === 0);
534
+ shutdownDrain.notify(!wedgedOp && pendingWriteDepth() === 0 && inFlightWrites() === 0);
533
535
  }
534
536
  }
535
537
 
@@ -650,6 +652,7 @@ async function dispatchWorker(entry: Submission, back: WorkerBackend): Promise<v
650
652
 
651
653
  // retrySafe stays false (default): a write must NEVER be auto-re-dispatched (it would repeat its
652
654
  // effect). No pool timeout is armed — the deadline is the gateway's own (§3.6.1).
655
+ const startedAt = performance.now();
653
656
  const poolPromise = back.pool.run<WriterJobInput, WriterJobResult>(WRITER_TASK_NAME, input, { labels });
654
657
 
655
658
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -665,13 +668,16 @@ async function dispatchWorker(entry: Submission, back: WorkerBackend): Promise<v
665
668
  // WEDGED (§3.6.1). The op is still running and still holds the lock; do NOT replace the worker.
666
669
  // Surface the caller's INDETERMINATE outcome, hold the FIFO, and resume when the op finally
667
670
  // settles (it commits/rolls back and releases the lock on the SAME warm connection).
668
- wedged = true;
671
+ // #3776 — name the op: "writer wedged" alone left an incident to guess which job held the lock.
672
+ wedgedOp = { op: entry.op, startedAt };
673
+ console.warn(`[writer-gateway] writer op ${entry.op} has held the writer past its ${writerTimeoutMs}ms deadline; writes are held until it settles`);
669
674
  rejectIndeterminate(entry);
670
675
  poolPromise.then(
671
676
  () => {},
672
677
  () => {},
673
678
  ).finally(() => {
674
- wedged = false;
679
+ console.warn(`[writer-gateway] wedged writer op ${entry.op} settled after ${Math.round(performance.now() - startedAt)}ms`);
680
+ wedgedOp = undefined;
675
681
  kickDrain();
676
682
  }).catch((error) => console.error(`[writer-gateway] resume after wedge failed: ${errMessage(error)}`));
677
683
  return;
@@ -1,5 +1,5 @@
1
- import type { HostView, Orchestrator } from "@humwick/sdk";
2
- import type { ProviderCatalogEntry } from "@humwick/sdk/provider-catalog";
1
+ import { errMessage, type HostView, type Orchestrator } from "@humwick/sdk";
2
+ import { resolveProviderSelection, type ProviderCatalogEntry } from "@humwick/sdk/provider-catalog";
3
3
  import type { DispatchTarget } from "./task-dispatcher";
4
4
  import { spawnPlacementRefusal, hostSpawnAdmission, hostIdentitiesById, spawnHostCapAdmission, type SpawnCapOptions } from "./spawn-placement";
5
5
  import { liveHostDisabledProviders } from "./host-provider-store";
@@ -17,13 +17,15 @@ export interface LiveTargetAdmission {
17
17
  targetUnavailableReason: (target: DispatchTarget) => string | undefined;
18
18
  }
19
19
 
20
- export function liveTargetAdmission(orchestrators: readonly Orchestrator[], views: readonly HostView[], orchestratorId?: string, capOpts: SpawnCapOptions = {}): LiveTargetAdmission {
20
+ export function liveTargetAdmission(orchestrators: readonly Orchestrator[], views: readonly HostView[], catalog: ProviderCatalogEntry[], orchestratorId?: string, capOpts: SpawnCapOptions = {}): LiveTargetAdmission {
21
21
  const identities = hostIdentitiesById(views);
22
22
  const byId = new Map(views.map((view) => [view.id, view]));
23
23
  const disabled = liveHostDisabledProviders();
24
24
  const capBlock = spawnHostCapAdmission(capOpts);
25
25
  const hosts = orchestrators.filter((host) => !orchestratorId || host.id === orchestratorId);
26
26
  const targetAdmission = ({ provider, model, effort }: DispatchTarget): TargetAdmission => {
27
+ try { resolveProviderSelection({ provider, model, effort }, catalog); }
28
+ catch { return { eligible: false, uncertainties: [] }; }
27
29
  let eligible = false;
28
30
  const uncertainties: TargetUncertainty[] = [];
29
31
  for (const host of hosts) {
@@ -34,9 +36,11 @@ export function liveTargetAdmission(orchestrators: readonly Orchestrator[], view
34
36
  }
35
37
  return { eligible, uncertainties };
36
38
  };
37
- const targetUnavailableReason = (target: DispatchTarget) => spawnPlacementRefusal(
38
- orchestrators, views, target.provider, target.model, target.effort, orchestratorId, capOpts,
39
- );
39
+ const targetUnavailableReason = (target: DispatchTarget) => {
40
+ try { resolveProviderSelection(target, catalog); }
41
+ catch (error) { return errMessage(error); }
42
+ return spawnPlacementRefusal(orchestrators, views, target.provider, target.model, target.effort, orchestratorId, capOpts);
43
+ };
40
44
  return { targetAdmission, targetUnavailableReason };
41
45
  }
42
46
 
@@ -1,7 +1,7 @@
1
1
  /** Precondition setup for the packaged-ephemeral landing-ingress Proof of Life.
2
2
  * Delivery and both crash boundaries run through the packaged Relay; this fixture only registers
3
3
  * the disposable workspace that its real ephemeral orchestrator will probe. */
4
- import { initDb } from "../db/schema.ts";
4
+ import { closeDb, openExistingDb } from "../db/connection.ts";
5
5
  import { upsertWorkspace } from "../db/workspaces.ts";
6
6
 
7
7
  const dbPath = process.env.DB_PATH;
@@ -13,7 +13,8 @@ if (!dbPath || !worktreePath || !orchestratorId || !baseSha) {
13
13
  throw new Error("usage: landing-ingress-proof-worker <worktree-path> <orchestrator-id> <base-sha>");
14
14
  }
15
15
 
16
- initDb(dbPath);
16
+ // The installed Relay already owns the schema; a fixture must not migrate its live DB (#3715).
17
+ openExistingDb(dbPath);
17
18
  upsertWorkspace({
18
19
  id: workspaceId,
19
20
  repoRoot: worktreePath,
@@ -27,4 +28,5 @@ upsertWorkspace({
27
28
  orchestratorId,
28
29
  metadata: {},
29
30
  });
31
+ closeDb();
30
32
  console.log(JSON.stringify({ workspaceId }));
@@ -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,