@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
@@ -1,938 +0,0 @@
1
- import {
2
- OpError,
3
- type Clock,
4
- type JsonValue,
5
- type ModuleCtx,
6
- type RetryPolicy,
7
- type Rng,
8
- type StepResult,
9
- type WaitCondition,
10
- type WorkflowClient,
11
- type WorkflowDecl,
12
- type WorkflowRun,
13
- type WorkflowStartOptions,
14
- type WorkflowStatusName,
15
- } from "./contract";
16
- import type { AsyncEntitySession } from "./async-entity-persistence";
17
- import type { AsyncEventSession } from "./async-event-log";
18
- import { MessageAuthorityCommitRefusedError, type MessageAuthoritySession } from "./message-authority";
19
- import {
20
- foldWorkflowJournal,
21
- workflowInstanceFromRow,
22
- workflowJournalRecordFromRow,
23
- type WorkflowResumePoint,
24
- type WorkflowWaitCondition,
25
- } from "./workflow-journal-codec";
26
- import {
27
- WorkflowContendedError,
28
- WorkflowLeaseLostError,
29
- WorkflowStepConflictError,
30
- type WorkflowInstanceRow,
31
- } from "./workflow-journal";
32
- import { type WorkflowStepCommitFaults } from "./workflow-step-commit";
33
- import type { KernelPersistence } from "./kernel-persistence";
34
- import { recoverIndeterminate } from "./kernel-persistence";
35
- import { armCompatibilityStepFaults } from "./sqlite-kernel-persistence";
36
- import { createWorkflowLeaseOwner } from "./workflow-lease-owner";
37
-
38
- /**
39
- * The predicate the M3 wakeup pass consults to decide whether a `waiting` instance's
40
- * {@link WaitCondition} is satisfied. A sleeper's dueness is a pure clock comparison the runtime
41
- * owns; a waiter's dueness depends on events/commands the kernel journal does NOT own, so the host
42
- * injects this seam (backed by the event log / command status). It must be a pure, side-effect-free
43
- * read: the runtime calls it while scanning and again under the acquired lease before firing, so it
44
- * may be evaluated more than once. Defaults to "never satisfied" when the host wires no evaluator.
45
- */
46
- export type WaitEvaluator = (waitFor: WaitCondition, instanceId: string) => boolean | Promise<boolean>;
47
-
48
- /**
49
- * Compute the durable backoff delay (ms) to wait before the NEXT attempt of a step whose attempt
50
- * `failedAttempt` (1-based) just failed. `fixed` is constant; `exponential` grows
51
- * `initialMs · multiplier^(failedAttempt-1)` capped at `maxMs` (default multiplier 2). Optional
52
- * jitter rides the injected {@link Rng} so it stays deterministic under a seeded/controllable rng.
53
- * A retry that must wait becomes a `sleeping` row the wakeup pass fires — never a busy-loop.
54
- */
55
- export function retryBackoffMs(policy: RetryPolicy, failedAttempt: number, rng: Rng): number {
56
- const backoff = policy.backoff;
57
- let base: number;
58
- if (backoff.kind === "fixed") {
59
- base = backoff.delayMs;
60
- } else {
61
- const multiplier = backoff.multiplier ?? 2;
62
- const grown = backoff.initialMs * multiplier ** Math.max(0, failedAttempt - 1);
63
- base = Math.min(grown, backoff.maxMs);
64
- }
65
- if (policy.jitter !== undefined && policy.jitter > 0) base = rng.jitter(base, policy.jitter);
66
- return Math.max(0, Math.round(base));
67
- }
68
-
69
- /**
70
- * The default executor lease TTL. A driver acquires a lease before running any step body and
71
- * renews it every iteration while it makes progress, so this window only needs to comfortably
72
- * outlast a single step's run+commit. It is injectable ({@link WorkflowRuntimeDeps.leaseTtlMs}) so
73
- * tests can shrink it to drive expiry/reclaim deterministically under a controllable clock.
74
- */
75
- const DEFAULT_EXECUTOR_LEASE_TTL_MS = 3_600_000;
76
-
77
- /** Discard a step's buffered sessions after a failed/aborted commit, tolerating a session the
78
- * rolled-back flush already finalized (double-discard would otherwise throw). Nothing reached
79
- * storage — the transaction rolled back — so this only drops in-memory buffers. */
80
- function discardSessions(entitySession: AsyncEntitySession<unknown>, eventSession: AsyncEventSession<unknown>, messageAuthoritySession?: MessageAuthoritySession): void {
81
- for (const session of [entitySession, eventSession, messageAuthoritySession]) {
82
- if (!session) continue;
83
- try {
84
- session.discard();
85
- } catch {
86
- // The session was already finalized by the (rolled-back) flush; its buffer is moot.
87
- }
88
- }
89
- }
90
-
91
- /** The fresh per-step context and the sessions that carry that step's K4/K5 effects to commit. */
92
- export interface WorkflowStepContext {
93
- readonly ctx: ModuleCtx;
94
- readonly entitySession: AsyncEntitySession<unknown>; readonly eventSession: AsyncEventSession<unknown>;
95
- readonly messageAuthoritySession: MessageAuthoritySession;
96
- }
97
-
98
- export interface WorkflowRuntimeDeps {
99
- readonly persistence: KernelPersistence;
100
- /** Module catalog install barrier; every operation awaits it before touching storage. */
101
- readonly ready: Promise<void>;
102
- readonly clock: Clock;
103
- readonly rng: Rng;
104
- readonly moduleName: string;
105
- /** The creator's actor (from `ctx.principal.actor`), stamped into `workflow_instances.created_by`
106
- * when this runtime seeds a new instance at {@link WorkflowRuntime.start}. The generic owner column
107
- * (KAPI-1 / decision 3) that unlocks a per-creator filter. Absent for system-driven runtimes → the
108
- * instance lands `created_by = NULL` (accepted creator-invisible divergence). */
109
- readonly createdBy?: string | null;
110
- /** The declaring module's workflows, keyed by name (from the manifest). */
111
- readonly workflows: Readonly<Record<string, WorkflowDecl>>;
112
- /** Build a fresh `ModuleCtx` + its entity/event sessions for one step attempt, bound to the
113
- * declaring module's principal/config (the same assembly the op path uses). */
114
- readonly beginStepContext: () => WorkflowStepContext;
115
- /** Executor lease TTL in ms; defaults to {@link DEFAULT_EXECUTOR_LEASE_TTL_MS}. */
116
- readonly leaseTtlMs?: number;
117
- /** Decides whether a `waiting` instance's {@link WaitCondition} is satisfied; see
118
- * {@link WaitEvaluator}. Defaults to "never satisfied" (waits only fire once the host wires one). */
119
- readonly isWaitSatisfied?: WaitEvaluator;
120
- /** Crash/fault-injection seam at the step-commit boundary (the M0 fault hooks, wired through to
121
- * the executor). Returns the faults to arm for a given attempt, or `undefined` for none. M4's
122
- * kill-9 chaos harness drives this; M3 uses it only to prove per-instance loop isolation. */
123
- readonly commitFaults?: (instanceId: string, stepName: string) => WorkflowStepCommitFaults | undefined;
124
- }
125
-
126
- /** How {@link WorkflowRuntime.#drive} treats a PARKED (`sleeping`/`waiting`) resume point: a normal
127
- * forward driver leaves parked work alone, whereas the M3 wakeup pass fires it once it is due. */
128
- type DriveMode = "forward" | "wake";
129
-
130
- type AnyWorkflowDecl = WorkflowDecl<unknown, JsonValue>;
131
-
132
- interface StepTransition {
133
- readonly state: JsonValue;
134
- readonly next: string;
135
- readonly nextStatus: WorkflowStatusName;
136
- readonly sleepUntil?: number;
137
- readonly waitFor?: WaitCondition;
138
- readonly output?: unknown;
139
- readonly hasOutput: boolean;
140
- }
141
-
142
- function errMessage(error: unknown): string {
143
- return error instanceof Error ? error.message : String(error);
144
- }
145
-
146
- /** The stored wait condition (validated at write time by `#validateWaitCondition`) as its contract
147
- * type, for handing to the injected {@link WaitEvaluator}. */
148
- function waitConditionOf(value: WorkflowWaitCondition): WaitCondition {
149
- return value as unknown as WaitCondition;
150
- }
151
-
152
- /**
153
- * A `start` whose seed/drive are SPLIT so a host can enlist the seed in an op's atomic commit
154
- * (`WorkflowStartOptions.enlist: "op-commit"`, T-0 hardening #1409 finding 1). Input/state are
155
- * already validated (a schema failure threw a typed `invalid_input` {@link OpError} at prepare
156
- * time), so `seed` is a pure idempotent insert the enclosing transaction owns.
157
- */
158
- export interface PreparedWorkflowStart {
159
- /** The projection handed back to the enlisted caller at prepare time: the LIVE run when the
160
- * idempotency key already names an instance, else a `pending` stub carrying the id the seed
161
- * envelope proposes (a concurrent same-key seed dedupes inside the writer job). */
162
- readonly provisional: WorkflowRun;
163
- /** Clone-safe seed data. The persistence writer executes it inside the enlisting op commit and
164
- * returns the authoritative id (including an idempotency-key dedupe result). */
165
- readonly seed: import("./workflow-journal").WorkflowInstanceSeed;
166
- /** Drive the seeded instance forward under the single-writer lease (a contended or terminal
167
- * instance drives zero step bodies). Call only AFTER the enlisting transaction committed. */
168
- readonly drive: (instanceId: string) => Promise<void>;
169
- }
170
-
171
- /**
172
- * Whether a step-body throw is a transient (retryable) failure rather than a permanent one. The
173
- * ratified contract has no error taxonomy to change, so the kernel applies a conservative rule: a
174
- * `TypeError` signals a deterministic programming/contract bug (retrying re-throws it), and a step
175
- * may explicitly opt a specific error out of retry by tagging it `retryable: false`. Everything else
176
- * is treated as transient and retried within the step's `RetryPolicy` bound. (Retries are still
177
- * gated by `maxAttempts`, so even a mis-classified permanent error terminates after exhaustion.)
178
- */
179
- function isRetryableError(error: unknown): boolean {
180
- if (error instanceof TypeError) return false;
181
- if (error !== null && typeof error === "object" && (error as { retryable?: unknown }).retryable === false) return false;
182
- return true;
183
- }
184
-
185
- function failureText(failure: unknown): string {
186
- if (typeof failure === "string") return failure;
187
- if (failure !== null && typeof failure === "object" && !Array.isArray(failure)) {
188
- const error = (failure as { error?: unknown }).error;
189
- if (typeof error === "string") return error;
190
- }
191
- return JSON.stringify(failure) ?? "unknown error";
192
- }
193
-
194
- /**
195
- * The workflow executor + `WorkflowClient` (start/status/cancel) plus the M2 recovery path. Given
196
- * a started instance, it acquires a fresh single-writer lease, then drives the `WorkflowDecl` step
197
- * graph: fold the journal for the resume state, run the next un-memoized step to a `StepResult`,
198
- * validate it, then commit the step's K4/K5 effects AND its journal memo row atomically through
199
- * M0's {@link commitStepAttempt}. It transitions per the result (`next`/`sleepUntil`/`waitFor`/
200
- * done) and continues until the instance completes or suspends. A step whose memo row already
201
- * exists is never re-run — the fold advances past it, so a same-key re-`start` and a re-drive both
202
- * execute zero steps.
203
- *
204
- * M2 single-writer semantics: a driver must {@link WorkflowJournalPersistence.acquireLease | win a
205
- * lease} before running any step body. Two concurrent same-key drivers therefore serialize — one
206
- * acquires and runs, the second fails to acquire and runs ZERO step bodies. The holder
207
- * {@link WorkflowJournalPersistence.renewLease | renews} each iteration so a slow drive cannot lose
208
- * the lease mid-progress. {@link WorkflowRuntime.recover} clears confirmed-dead owner incarnations
209
- * immediately, or reclaims expired leases, then resumes from the memoized state. If a commit
210
- * loses the fence (a zombie driver whose lease was reclaimed), the buffered sessions are discarded
211
- * and the driver relinquishes cleanly, leaving the instance reclaimable rather than marking it
212
- * failed for merely losing a race.
213
- */
214
- export class WorkflowRuntime implements WorkflowClient<unknown> {
215
- readonly #deps: WorkflowRuntimeDeps;
216
- readonly #leaseTtlMs: number;
217
- readonly #owner: string;
218
- readonly #isWaitSatisfied: WaitEvaluator;
219
- /** Leases this driver is actively holding mid-drive (instanceId → token), so {@link heartbeat}
220
- * can renew a long-running body's lease from the injected clock without a live JS timer. */
221
- readonly #active = new Map<string, string>();
222
-
223
- constructor(deps: WorkflowRuntimeDeps) {
224
- this.#deps = deps;
225
- this.#leaseTtlMs = deps.leaseTtlMs ?? DEFAULT_EXECUTOR_LEASE_TTL_MS;
226
- this.#isWaitSatisfied = deps.isWaitSatisfied ?? (() => false);
227
- // The owner records process incarnation for recovery; the per-drive token fences commits.
228
- this.#owner = createWorkflowLeaseOwner(`${deps.moduleName}:${deps.rng.uuid()}`);
229
- }
230
-
231
- async start<N extends string>(name: N, input: unknown, options: WorkflowStartOptions): Promise<WorkflowRun> {
232
- // Enlistment needs an op commit to join; only the module host's op invocation path provides one
233
- // (it routes enlisted starts through prepareStart itself, never here). Fail closed everywhere
234
- // else — a step body or standalone client asking for atomicity that does not exist must not get
235
- // a silent immediate commit instead.
236
- if (options?.enlist === "op-commit") {
237
- throw new Error("workflow start enlist:'op-commit' is only available inside an op invocation");
238
- }
239
- const prepared = await this.prepareStart(name, input, options);
240
-
241
- // Own commit (the ratified default), deduped on UNIQUE(module, workflow, idempotency_key): the
242
- // same key returns the same run and never seeds a second instance. The instance is seeded
243
- // UNLEASED — the driver below acquires its own fresh lease, so two concurrent same-key starts
244
- // contend on the acquire (one wins, one runs zero step bodies).
245
- await this.#deps.ready;
246
- const requestKey = this.#deps.rng.uuid(), capturedAt = this.#deps.clock.now();
247
- const seed = structuredClone(prepared.seed);
248
- const { instance } = await recoverIndeterminate(() => this.#deps.persistence.seedWorkflow(requestKey, seed, capturedAt));
249
- const instanceId = instance.id;
250
-
251
- // In-process there is no separate executor yet, so `start` IS the driver: acquire the lease and
252
- // drive until the instance completes or parks on its first suspension, then return. A dedupe hit
253
- // that is already terminal (or held live by a concurrent driver) drives zero steps. Durable
254
- // firing of a parked suspension is M3.
255
- await prepared.drive(instanceId);
256
- return (await this.status(instanceId))!;
257
- }
258
-
259
- /**
260
- * Validate + stage a `start` WITHOUT committing anything: the returned {@link PreparedWorkflowStart}
261
- * splits the idempotent instance SEED envelope (which the writer executes inside the caller's
262
- * T-0 enlisted-op-commit job) from the post-commit DRIVE. All validation happens HERE, eagerly:
263
- * a workflow-input/state schema failure throws a typed `invalid_input` {@link OpError} (#1409
264
- * finding 2 — a deterministic validation failure must be classifiable by callers, never a raw
265
- * ZodError that reads as transient), so an enlisting op aborts before buffering a seed at all.
266
- */
267
- async prepareStart(name: string, input: unknown, options: WorkflowStartOptions): Promise<PreparedWorkflowStart> {
268
- const decl = this.#deps.workflows[name] as AnyWorkflowDecl | undefined;
269
- if (!decl) throw new Error(`module '${this.#deps.moduleName}' declares no workflow '${name}'`);
270
- const idempotencyKey = options?.idempotencyKey;
271
- if (typeof idempotencyKey !== "string" || idempotencyKey.length === 0) {
272
- throw new TypeError("workflow start requires a non-empty idempotencyKey");
273
- }
274
-
275
- const qualified = `${this.#deps.moduleName}.${name}`;
276
- const parsedInput = this.#parseAsInvalidInput(() => decl.input.parse(input), `workflow '${qualified}' input`) as unknown;
277
- const initialState = this.#parseAsInvalidInput(
278
- () => decl.state.parse(decl.init(parsedInput)),
279
- `workflow '${qualified}' initial state`,
280
- ) as JsonValue;
281
-
282
- const seedId = this.#deps.rng.uuid();
283
- await this.#deps.ready;
284
- const existing = await this.#deps.persistence.getWorkflowInstanceByIdentity(this.#deps.moduleName, name, idempotencyKey);
285
- const now = this.#deps.clock.now();
286
- const provisional: WorkflowRun = existing
287
- ? await this.#projectRun(existing)
288
- : { id: seedId, status: "pending", attempts: 0, createdAt: now, updatedAt: now };
289
- return {
290
- provisional,
291
- seed: {
292
- id: seedId, moduleName: this.#deps.moduleName, workflowName: name, idempotencyKey,
293
- input: parsedInput as JsonValue, initialState,
294
- startStep: decl.start, status: "pending", createdBy: this.#deps.createdBy ?? null,
295
- },
296
- drive: async (instanceId: string) => {
297
- await this.#driveWithLease(instanceId, decl);
298
- },
299
- };
300
- }
301
-
302
- /** Run a schema parse, converting a deterministic validation failure (ZodError) into the typed
303
- * `invalid_input` {@link OpError} the op/apply layers classify on. Non-validation throws (a
304
- * buggy `init`, an exotic schema) propagate unchanged — they are not input classifications. */
305
- #parseAsInvalidInput<T>(parse: () => T, what: string): T {
306
- try {
307
- return parse();
308
- } catch (error) {
309
- if (error instanceof Error && error.name === "ZodError") {
310
- throw new OpError("invalid_input", 400, `${what} failed validation: ${errMessage(error)}`);
311
- }
312
- throw error;
313
- }
314
- }
315
-
316
- /**
317
- * The M2 recovery entry point for ONE instance: reclaim its lease (if unleased/expired) and
318
- * resume driving from the folded journal to completion or its next suspension. Committed steps
319
- * are never re-run. A supervisor calls this (or {@link WorkflowRuntime.recover}) to pick up an
320
- * instance orphaned by a crashed driver. Returns the projected run, or `undefined` if the
321
- * instance is absent. If the lease is currently held live by another driver, the acquire fails
322
- * and this resumes zero steps — the live driver keeps ownership.
323
- */
324
- async resume(id: string): Promise<WorkflowRun | undefined> {
325
- await this.#deps.ready;
326
- const row = await this.#deps.persistence.getWorkflowInstance(id);
327
- if (!row) return undefined;
328
- const decl = this.#deps.workflows[row.workflow_name] as AnyWorkflowDecl | undefined;
329
- // A missing decl means this runtime does not own that workflow; leave it for a runtime that does.
330
- if (decl) await this.#driveWithLease(id, decl);
331
- return this.status(id);
332
- }
333
-
334
- /**
335
- * The M2 recovery driver: clear leases of confirmed-dead local owners, then scan for open,
336
- * drivable instances whose lease is absent or expired. This is the boot + interval job of S4 §6.4
337
- * — a supervisor invokes it on startup and on a tick to reclaim orphaned work. Returns the ids it
338
- * drove. Parked `sleeping`/`waiting` instances are intentionally excluded (their wakeup is M3).
339
- */
340
- async recover(): Promise<string[]> {
341
- await this.#deps.ready;
342
- const now = this.#deps.clock.now();
343
- await recoverIndeterminate(() => this.#deps.persistence.reclaimDeadWorkflowLeases(this.#deps.moduleName, now));
344
- return this.#drivePass(await this.#deps.persistence.listResumableWorkflows(this.#deps.moduleName, this.#deps.clock.now()), "forward");
345
- }
346
-
347
- /**
348
- * A SINGLE-snapshot classification of this module's residual OPEN work for a boot/OFF reconcile that has
349
- * no ticker (#1447 finding C, round-7). Samples the injected clock EXACTLY ONCE and issues ONE journal
350
- * query ({@link WorkflowJournalPersistence.snapshotReconcileResidual}), so the reclaimable-now count and
351
- * the live-lease recovery delay are computed against ONE `now` and CANNOT disagree across the
352
- * lease-expiry boundary — the round-6 false-convergence (a reclaimable read at E-1 = 0 AND an
353
- * independent live-lease read at E = undefined, from two separate clock samples, wrongly declaring
354
- * "converged" and stranding an acknowledged close) is impossible by construction. READ-ONLY (no lease
355
- * mutation).
356
- * • `reclaimableCount` — OPEN instances {@link recover} could drive RIGHT NOW (unleased or expired). If
357
- * a sweep drove nothing yet this is > 0, the acquire was transiently CONTENDED (not "no work") — the
358
- * reconcile must retry (a short backoff), not declare a false convergence and strand the close.
359
- * • `liveLeaseRecoveryDelayMs` — ms (≥ 0) until this module's earliest STILL-LIVE crash lease lapses
360
- * and becomes plain-{@link recover}-reclaimable, or `undefined` when no open instance holds a live
361
- * lease. A step killed BEFORE its journal commit leaves a `pending` instance still holding the dead
362
- * driver's lease. Recovery clears confirmed-dead local incarnations before this read; legacy,
363
- * remote or unverifiable owners still require a bounded retry at expiry. It never force-clears
364
- * a lease that might belong to a LIVE concurrent process
365
- * (round-4: clearing a live lease would double-drive its step body, breaking the single-writer
366
- * invariant).
367
- */
368
- async classifyReconcileResidual(): Promise<{ reclaimableCount: number; liveLeaseRecoveryDelayMs: number | undefined }> {
369
- const now = this.#deps.clock.now();
370
- await this.#deps.ready;
371
- const { reclaimableCount, nextLiveLeaseExpiry } = await this.#deps.persistence.snapshotWorkflowResidual(this.#deps.moduleName, now);
372
- return { reclaimableCount, liveLeaseRecoveryDelayMs: nextLiveLeaseExpiry === undefined ? undefined : Math.max(0, nextLiveLeaseExpiry - now) };
373
- }
374
-
375
- /**
376
- * The M3 timer/wait wakeup pass — the firing counterpart to {@link recover}. Driven by the
377
- * injected clock (there is NO live `setTimeout`), it finds DUE parked instances and drives them
378
- * forward: `sleeping` rows whose `sleep_until <= now` (durable timers AND retry backoffs both park
379
- * this way), plus `waiting` rows whose {@link WaitCondition} the injected evaluator reports
380
- * satisfied. Each is acquired FIRST (same single-writer lease discipline as recovery, so a wakeup
381
- * and a recover can never double-drive one instance) and then fired. This realizes the timers
382
- * pilot's deferred `fired` as a sleeping-row a pass claims. Returns the ids it genuinely acquired
383
- * and drove — a candidate a rival grabbed first, or that is no longer due under the lease, is not
384
- * reported. A supervisor calls this on a clock tick; before that, it drives ZERO.
385
- */
386
- async wakeup(now = this.#deps.clock.now()): Promise<string[]> {
387
- await this.#deps.ready;
388
- const sleepers = await this.#deps.persistence.listDueWorkflows(this.#deps.moduleName, now);
389
- const waiters: string[] = [];
390
- for (const id of await this.#deps.persistence.listWaitingWorkflows(this.#deps.moduleName, now)) {
391
- if (await this.#waiterSatisfied(id, now)) waiters.push(id);
392
- }
393
- // Dedup in case a status raced between the two queries; preserve sleeper-first ordering.
394
- const due = [...new Set([...sleepers, ...waiters])];
395
- return this.#drivePass(due, "wake");
396
- }
397
-
398
- /**
399
- * Drive a work-list of instance ids under `mode`, ISOLATING each instance (M2 review residual 2):
400
- * one instance's non-lease failure (e.g. an entity OCC conflict re-thrown from its commit) is
401
- * caught and the pass continues to the next — it never aborts the whole tick and strands later
402
- * due work. Only instances this call GENUINELY acquired and drove are reported (residual 4): a
403
- * failed acquire (contended/terminal) or a mid-drive throw is excluded, so the returned list is an
404
- * honest record of what this pass advanced, which the recovery/wakeup metrics count.
405
- */
406
- async #drivePass(ids: readonly string[], mode: DriveMode): Promise<string[]> {
407
- const driven: string[] = [];
408
- for (const id of ids) {
409
- const row = await this.#deps.persistence.getWorkflowInstance(id);
410
- if (!row) continue;
411
- const decl = this.#deps.workflows[row.workflow_name] as AnyWorkflowDecl | undefined;
412
- if (!decl) continue;
413
- try {
414
- if (await this.#driveWithLease(id, decl, mode)) driven.push(id);
415
- } catch {
416
- // Residual 2: a per-instance failure must not abort the pass. The lease was already
417
- // released in #driveWithLease's finally, so a later tick can retry this instance.
418
- }
419
- }
420
- return driven;
421
- }
422
-
423
- /**
424
- * Renew every lease this driver is actively holding mid-drive, from the injected clock. This is
425
- * the residual-1 heartbeat: a step body slower than one lease TTL would otherwise let its OWN
426
- * lease lapse (and self-fence at commit) — a supervisor calls this on a clock tick to keep a
427
- * long-running body's lease alive WITHOUT any live `setInterval`. Returns the ids renewed; a lease
428
- * already reclaimed by a rival (token rotated) simply fails to renew and is omitted.
429
- */
430
- async heartbeat(now = this.#deps.clock.now()): Promise<string[]> {
431
- const renewed: string[] = [];
432
- for (const [instanceId, token] of this.#active) {
433
- const requestKey = this.#deps.rng.uuid();
434
- if (await recoverIndeterminate(() => this.#deps.persistence.renewWorkflowLease(requestKey, { instanceId, token, now, ttlMs: this.#leaseTtlMs }))) renewed.push(instanceId);
435
- }
436
- return renewed;
437
- }
438
-
439
- /** Pre-screen a `waiting` candidate from its row's `wait_for`, written in the same commit as the
440
- * journal memo. The full journal fold runs only under the acquired lease in #drive (#3006). */
441
- async #waiterSatisfied(instanceId: string, _now: number): Promise<boolean> {
442
- const row = await this.#deps.persistence.getWorkflowInstance(instanceId);
443
- if (!row || row.status !== "waiting") return false;
444
- const { waitFor } = workflowInstanceFromRow(row);
445
- if (waitFor === undefined) return false;
446
- return this.#isWaitSatisfied(waitConditionOf(waitFor), instanceId);
447
- }
448
-
449
- async status(id: string): Promise<WorkflowRun | undefined> {
450
- const row = await this.#deps.persistence.getWorkflowInstance(id);
451
- if (!row) return undefined;
452
- return this.#projectRun(row);
453
- }
454
-
455
- /** Project one instance row to its contract {@link WorkflowRun} (shared by `status` and the
456
- * prepare-time provisional for an already-existing identity). */
457
- async #projectRun(row: WorkflowInstanceRow): Promise<WorkflowRun> {
458
- const instance = workflowInstanceFromRow(row);
459
- // The cursor counts every memoized row (the fold enforces it); a prune deletes through it (#3006).
460
- const attempts = instance.journalCursor - instance.journalPrunedThrough;
461
- return {
462
- id: instance.id,
463
- status: instance.status,
464
- attempts,
465
- createdAt: instance.createdAt,
466
- updatedAt: instance.updatedAt,
467
- ...(instance.status === "done" && instance.output !== undefined ? { output: instance.output } : {}),
468
- ...(instance.failure !== undefined ? { error: failureText(instance.failure) } : {}),
469
- };
470
- }
471
-
472
- async cancel(id: string): Promise<boolean> {
473
- return this.control("cancel", id);
474
- }
475
-
476
- async control(operation: "cancel" | "fail" | "reopen", id: string, failure?: unknown): Promise<boolean> {
477
- const requestKey = this.#deps.rng.uuid();
478
- const now = this.#deps.clock.now();
479
- return recoverIndeterminate(() => this.#deps.persistence.controlWorkflow(requestKey, operation, id, now, failure));
480
- }
481
-
482
- /**
483
- * Acquire the single-writer lease, then drive, then release. Acquire is the mutual-exclusion
484
- * point: it succeeds only when the instance is unleased or its lease has expired, so a second
485
- * concurrent driver of the same instance FAILS HERE and runs zero step bodies. This is also the
486
- * crash-recovery reclaim — an expired lease left by a dead driver is taken over with a fresh
487
- * token. When {@link WorkflowRuntime.#drive} returns `true` we still hold the lease and release
488
- * it (so a parked instance can be reacquired by M3 without waiting out the TTL); when it returns
489
- * `false` we already lost the fence and there is nothing of ours to release.
490
- */
491
- async #driveWithLease(instanceId: string, decl: AnyWorkflowDecl, mode: DriveMode = "forward"): Promise<boolean> {
492
- const token = this.#deps.rng.uuid();
493
- let acquired: boolean;
494
- try {
495
- const requestKey = this.#deps.rng.uuid();
496
- const now = this.#deps.clock.now();
497
- acquired = await recoverIndeterminate(() => this.#deps.persistence.acquireWorkflowLease(requestKey, {
498
- instanceId,
499
- owner: this.#owner,
500
- token,
501
- now,
502
- ttlMs: this.#leaseTtlMs,
503
- }));
504
- } catch (error) {
505
- // A SQLITE_BUSY on the acquire itself is a clean back-off: we did NOT take the lease (the
506
- // UPDATE never landed), so treat it exactly like a contended/terminal acquire — drive nothing,
507
- // let a later tick retry. Never leaks as an untyped error that would abort the pass.
508
- if (error instanceof WorkflowContendedError) return false;
509
- throw error;
510
- }
511
- // Contended (a live lease held by another driver) or terminal (no open row to claim): another
512
- // writer owns this instance, or there is nothing to drive. Either way, run no step body and
513
- // report that we did NOT drive it (so a pass never over-reports what it advanced).
514
- if (!acquired) return false;
515
-
516
- // Register the live lease so heartbeat() can renew it mid-body from the injected clock.
517
- this.#active.set(instanceId, token);
518
- let holdsLease = true;
519
- try {
520
- holdsLease = await this.#drive(instanceId, decl, token, mode);
521
- } catch (error) {
522
- // A SQLITE_BUSY on a renew or the commit tx propagates here as WorkflowContendedError. Nothing
523
- // of ours committed (the tx rolled back), and we still hold our lease — so this is a typed
524
- // relinquish, NOT a failure: fall through to the finally, which RELEASES our lease so a later
525
- // tick/recovery reclaims and re-runs the same un-memoized step. The instance is never marked
526
- // failed for merely losing a storage race.
527
- if (!(error instanceof WorkflowContendedError)) throw error;
528
- holdsLease = true;
529
- return false;
530
- } finally {
531
- this.#active.delete(instanceId);
532
- if (holdsLease) {
533
- const requestKey = this.#deps.rng.uuid();
534
- const now = this.#deps.clock.now();
535
- await recoverIndeterminate(() => this.#deps.persistence.releaseWorkflowLease(requestKey, instanceId, token, now));
536
- }
537
- }
538
- return true;
539
- }
540
-
541
- /**
542
- * Drive the step graph forward under lease `token` until the instance is terminal or parked on a
543
- * suspension. Returns whether we STILL hold the lease at exit: `true` after a normal
544
- * terminal/parked stop, `false` if we relinquished because a commit lost the fence (our lease was
545
- * reclaimed by another driver — a zombie-driver fence-off). The caller releases the lease only in
546
- * the `true` case.
547
- */
548
- async #drive(instanceId: string, decl: AnyWorkflowDecl, token: string, mode: DriveMode): Promise<boolean> {
549
- for (;;) {
550
- const row = await this.#deps.persistence.getWorkflowInstance(instanceId);
551
- if (!row) throw new Error(`workflow instance '${instanceId}' vanished mid-drive`);
552
- // Pause TOCTOU (second half): the acquire gated on `paused_at IS NULL`, but a pause can still
553
- // land AFTER a successful acquire and BEFORE we run the next step — or mid-drive between steps
554
- // of a forward drive. Re-check here so a paused instance runs NO new step: pause takes effect
555
- // as "no new step after the current one." We still hold the lease, so return `true` for the
556
- // caller to release it; the instance simply sits paused until resumed.
557
- if (row.paused_at !== null) return true;
558
- const instance = workflowInstanceFromRow(row);
559
- const records = (await this.#deps.persistence.listWorkflowSteps(instanceId)).map(workflowJournalRecordFromRow);
560
- const resume = foldWorkflowJournal(instance, records);
561
-
562
- // A `ready` resume has an un-memoized step to run. `done`/`failed`/`canceled` are terminal.
563
- // `sleeping`/`waiting` are parked: a forward driver leaves them be, but a `wake` pass fires
564
- // them ONCE they are DUE (timer elapsed / wait satisfied) — the same clock and predicate the
565
- // pass pre-screened with, re-checked here under the acquired lease so a "due but un-drivable"
566
- // gap cannot open. After the parked step runs the instance is `running`, so subsequent loop
567
- // iterations proceed as a normal forward drive.
568
- const runnable = resume.status === "ready"
569
- || (mode === "wake" && await this.#isDue(resume, this.#deps.clock.now(), instanceId));
570
- if (!runnable) return true;
571
- const stepName = resume.stepName;
572
- if (!stepName) return true;
573
-
574
- const step = decl.steps[stepName];
575
- if (!step) {
576
- await this.#fail(instanceId, `workflow '${instance.workflowName}' has no step '${stepName}'`);
577
- return true;
578
- }
579
-
580
- // Renew before running the step so a competitor cannot reclaim while a long body runs, and so
581
- // progress across steps keeps extending the window. A failed renew means our lease was already
582
- // reclaimed (we were too slow) — relinquish without running the body; the reclaiming driver
583
- // owns the instance now.
584
- if (!await this.#renew(instanceId, token)) {
585
- return false;
586
- }
587
-
588
- const { ctx, entitySession, eventSession, messageAuthoritySession } = this.#deps.beginStepContext();
589
- const startedAt = this.#deps.clock.now();
590
- let result: StepResult<JsonValue>;
591
- try {
592
- result = await step.run(ctx, resume.state as JsonValue);
593
- } catch (error) {
594
- // The step body threw. Its buffered effects never commit; classify the failure and either
595
- // retry (immediately, or parked on a backoff sleeping-row) or go terminal `failed`.
596
- entitySession.discard();
597
- eventSession.discard();
598
- messageAuthoritySession.discard();
599
- const outcome = await this.#handleStepFailure(instanceId, resume, stepName, step.retry, startedAt, token, error);
600
- if (outcome === "retry-loop") continue;
601
- return outcome === "relinquish" ? false : true;
602
- }
603
-
604
- let transition: StepTransition;
605
- try {
606
- transition = this.#validateTransition(decl, stepName, result);
607
- } catch (error) {
608
- // A malformed `StepResult` is a deterministic contract violation, never a transient fault —
609
- // retrying cannot help, so it is always permanent regardless of the step's retry policy.
610
- entitySession.discard();
611
- eventSession.discard();
612
- messageAuthoritySession.discard();
613
- const outcome = await this.#commitFailedAttempt(instanceId, resume, stepName, startedAt, token, errMessage(error), { terminal: true });
614
- return outcome === "relinquish" ? false : true;
615
- }
616
-
617
- // The memoized `result` JSON must round-trip through the journal codec, so it carries the
618
- // same sleepUntil/waitFor/output the transition columns record.
619
- const resultJson: Record<string, JsonValue> = { state: transition.state, next: transition.next };
620
- if (transition.hasOutput) resultJson.output = (transition.output ?? null) as JsonValue;
621
- if (transition.sleepUntil !== undefined) resultJson.sleepUntil = transition.sleepUntil;
622
- if (transition.waitFor !== undefined) resultJson.waitFor = transition.waitFor as unknown as JsonValue;
623
-
624
- // Residual 1 (renew cadence): renew right before the commit using the post-body clock, so a
625
- // body that alone outran the TTL still fences successfully AS LONG AS no rival actually
626
- // reclaimed. If the renew fails, a rival already rotated our token — relinquish; nothing of
627
- // ours committed. This makes the "body slower than the TTL self-fences" failure impossible
628
- // for a solo holder, while still yielding to a genuine reclaim.
629
- if (!await this.#renew(instanceId, token)) {
630
- discardSessions(entitySession, eventSession, messageAuthoritySession);
631
- return false;
632
- }
633
-
634
- // The four-way atomic commit: fence re-check + entity flush + event flush + journal memo,
635
- // all-or-nothing. No effect reaches storage on any side channel.
636
- const faults = this.#deps.commitFaults?.(instanceId, stepName);
637
- try {
638
- entitySession.seal();
639
- eventSession.seal();
640
- messageAuthoritySession.seal();
641
- const capturedAt = this.#deps.clock.now();
642
- const requestKey = this.#deps.rng.uuid();
643
- if (faults) armCompatibilityStepFaults(this.#deps.persistence, requestKey, faults);
644
- const envelope = structuredClone({
645
- requestKey,
646
- capturedAt,
647
- lease: { instanceId, token, now: capturedAt },
648
- entities: entitySession.mutations(),
649
- events: eventSession.appends(capturedAt),
650
- messageAuthority: messageAuthoritySession.operations(),
651
- record: {
652
- instanceId,
653
- stepIndex: resume.stepIndex,
654
- stepName,
655
- attempt: resume.nextAttempt,
656
- outcome: "completed" as const,
657
- state: transition.state,
658
- result: resultJson,
659
- nextStatus: transition.nextStatus,
660
- startedAt,
661
- sleepUntil: transition.sleepUntil ?? null,
662
- waitFor: transition.waitFor ?? null,
663
- output: transition.nextStatus === "done" && transition.hasOutput ? ((transition.output ?? null) as JsonValue) : null,
664
- },
665
- });
666
- await recoverIndeterminate(() => this.#deps.persistence.stepCommit(envelope));
667
- } catch (error) {
668
- // We lost the race for this step: either our lease was reclaimed/expired (fence failed) or
669
- // another driver already memoized this exact attempt (conflict). The transaction rolled
670
- // back — no effect of ours reached storage. Discard the buffered sessions and relinquish;
671
- // do NOT mark the instance failed, because a legitimate driver may be progressing it. Leave
672
- // it exactly as found so that driver (or a later reclaim) continues from the memoized state.
673
- if (error instanceof WorkflowLeaseLostError || error instanceof WorkflowStepConflictError) {
674
- discardSessions(entitySession, eventSession, messageAuthoritySession);
675
- return false;
676
- }
677
- if (error instanceof MessageAuthorityCommitRefusedError && error.refusal.code === "superseded") {
678
- // The participant proved that a higher fence/generation owns this timer. Its first
679
- // transaction rolled back every proposed domain effect; now memoize a domain-free done
680
- // transition so the obsolete due sleeper is reaped instead of re-driven every tick.
681
- discardSessions(entitySession, eventSession, messageAuthoritySession);
682
- const disposition = await this.#commitSupersededAttempt(instanceId, resume, stepName, startedAt, token, error);
683
- return disposition === "relinquish" ? false : true;
684
- }
685
- // Any other commit failure (e.g. an entity OCC conflict) is not a lost-lease condition;
686
- // drop the buffers and propagate — nothing was committed. The wakeup/recover pass isolates
687
- // this per-instance (residual 2) so it does not abort the whole tick.
688
- discardSessions(entitySession, eventSession, messageAuthoritySession);
689
- throw error;
690
- }
691
-
692
- // Terminal (done) or parked (sleeping/waiting): stop. Only `running` continues the loop, and
693
- // the next fold advances past the step we just memoized.
694
- if (transition.nextStatus !== "running") return true;
695
- }
696
- }
697
-
698
- /** Whether a PARKED resume point is due to fire now: a `sleeping` timer/backoff whose deadline has
699
- * passed, or a `waiting` condition the injected evaluator reports satisfied. */
700
- async #isDue(resume: WorkflowResumePoint, now: number, instanceId: string): Promise<boolean> {
701
- if (resume.status === "sleeping") return resume.sleepUntil !== undefined && resume.sleepUntil <= now;
702
- if (resume.status === "waiting") {
703
- return resume.waitFor !== undefined && await this.#isWaitSatisfied(waitConditionOf(resume.waitFor), instanceId);
704
- }
705
- return false;
706
- }
707
-
708
- /**
709
- * Classify a step-body throw and act. With a retry policy and attempts remaining (and a retryable
710
- * error), journal a `failed` attempt — bumping `attempt` under the UNIQUE(instance, step, attempt)
711
- * index so each retry is exactly-once — and either retry immediately (`retry-loop`, backoff 0) or
712
- * park a `sleeping` backoff row the wakeup pass fires. Otherwise (no policy, attempts exhausted, or
713
- * a non-retryable error) journal the final `failed` attempt and go terminal `failed`. Returns how
714
- * `#drive` should proceed. A lost lease during the commit yields `relinquish`.
715
- */
716
- async #handleStepFailure(
717
- instanceId: string,
718
- resume: WorkflowResumePoint,
719
- stepName: string,
720
- retry: RetryPolicy | undefined,
721
- startedAt: number,
722
- token: string,
723
- error: unknown,
724
- ): Promise<"retry-loop" | "stop" | "relinquish"> {
725
- const failedAttempt = resume.nextAttempt;
726
- const message = errMessage(error);
727
- const retryable = retry !== undefined && failedAttempt < retry.maxAttempts && isRetryableError(error);
728
- if (!retryable) {
729
- return this.#commitFailedAttempt(instanceId, resume, stepName, startedAt, token, message, { terminal: true });
730
- }
731
- const delayMs = retryBackoffMs(retry, failedAttempt, this.#deps.rng);
732
- return this.#commitFailedAttempt(instanceId, resume, stepName, startedAt, token, message, { terminal: false, delayMs });
733
- }
734
-
735
- /**
736
- * Journal one `failed` attempt atomically under the fence and advance the instance: to terminal
737
- * `failed` (`terminal`), to a `sleeping` backoff row due at `now + delayMs` (a retry that must
738
- * wait), or straight back to `running` for an immediate retry (`delayMs === 0`). Renews the lease
739
- * with the post-body clock first (residual 1) so a slow failing body still fences. Returns
740
- * `retry-loop` to re-run in the same drive, `stop` when parked/terminal, or `relinquish` on a lost
741
- * lease / attempt conflict (another writer owns the instance).
742
- */
743
- async #commitFailedAttempt(
744
- instanceId: string,
745
- resume: WorkflowResumePoint,
746
- stepName: string,
747
- startedAt: number,
748
- token: string,
749
- message: string,
750
- disposition: { terminal: true } | { terminal: false; delayMs: number },
751
- ): Promise<"retry-loop" | "stop" | "relinquish"> {
752
- if (!await this.#renew(instanceId, token)) {
753
- return "relinquish";
754
- }
755
- const failedAt = this.#deps.clock.now();
756
- const immediate = !disposition.terminal && disposition.delayMs <= 0;
757
- const nextStatus: WorkflowStatusName = disposition.terminal ? "failed" : immediate ? "running" : "sleeping";
758
- // M4 chaos seam: a kill point armed here fires while scheduling the backoff/failed row.
759
- const faults = this.#deps.commitFaults?.(instanceId, stepName);
760
- try {
761
- const requestKey = this.#deps.rng.uuid();
762
- if (faults) armCompatibilityStepFaults(this.#deps.persistence, requestKey, faults);
763
- const envelope = structuredClone({
764
- requestKey,
765
- capturedAt: failedAt,
766
- lease: { instanceId, token, now: failedAt },
767
- record: {
768
- instanceId,
769
- stepIndex: resume.stepIndex,
770
- stepName,
771
- attempt: resume.nextAttempt,
772
- outcome: "failed" as const,
773
- state: resume.state as JsonValue,
774
- result: { error: message },
775
- nextStatus,
776
- startedAt,
777
- sleepUntil: disposition.terminal || immediate ? null : failedAt + disposition.delayMs,
778
- waitFor: null,
779
- output: null,
780
- failure: disposition.terminal ? { error: message } : null,
781
- },
782
- });
783
- await recoverIndeterminate(() => this.#deps.persistence.failedStepCommit(envelope));
784
- } catch (error) {
785
- // Same race semantics as a completed commit: a fenced-off zombie or a duplicate attempt row
786
- // rolls back; relinquish and leave the instance for its rightful driver.
787
- if (error instanceof WorkflowLeaseLostError || error instanceof WorkflowStepConflictError) return "relinquish";
788
- throw error;
789
- }
790
- return immediate ? "retry-loop" : "stop";
791
- }
792
-
793
- /** Terminalize an exact authority generation that the host proved was superseded. Contention and
794
- * ordinary refusals never enter here: those retain the sleeping row and remain retryable. */
795
- async #commitSupersededAttempt(
796
- instanceId: string,
797
- resume: WorkflowResumePoint,
798
- stepName: string,
799
- startedAt: number,
800
- token: string,
801
- refusal: MessageAuthorityCommitRefusedError,
802
- ): Promise<"stop" | "relinquish"> {
803
- if (!await this.#renew(instanceId, token)) {
804
- return "relinquish";
805
- }
806
- const faults = this.#deps.commitFaults?.(instanceId, stepName);
807
- try {
808
- const capturedAt = this.#deps.clock.now();
809
- const requestKey = this.#deps.rng.uuid();
810
- if (faults) armCompatibilityStepFaults(this.#deps.persistence, requestKey, faults);
811
- const envelope = structuredClone({
812
- requestKey,
813
- capturedAt,
814
- lease: { instanceId, token, now: capturedAt },
815
- record: {
816
- instanceId,
817
- stepIndex: resume.stepIndex,
818
- stepName,
819
- attempt: resume.nextAttempt,
820
- outcome: "completed" as const,
821
- state: resume.state as JsonValue,
822
- result: {
823
- state: resume.state as JsonValue,
824
- next: "done",
825
- superseded: true,
826
- reason: refusal.refusal.reason,
827
- },
828
- nextStatus: "done" as const,
829
- startedAt,
830
- sleepUntil: null,
831
- waitFor: null,
832
- output: null,
833
- },
834
- });
835
- await recoverIndeterminate(() => this.#deps.persistence.supersededStepCommit(envelope));
836
- } catch (error) {
837
- if (error instanceof WorkflowLeaseLostError || error instanceof WorkflowStepConflictError) return "relinquish";
838
- throw error;
839
- }
840
- return "stop";
841
- }
842
-
843
- /** Validate a step's `StepResult` against the decl and project its durable transition. */
844
- #validateTransition(decl: AnyWorkflowDecl, stepName: string, result: StepResult<JsonValue>): StepTransition {
845
- if (result === null || typeof result !== "object") {
846
- throw new TypeError(`step '${stepName}' must return a StepResult object`);
847
- }
848
- const next = (result as { next?: unknown }).next;
849
- if (typeof next !== "string" || next.length === 0) {
850
- throw new TypeError(`step '${stepName}' must return a non-empty 'next'`);
851
- }
852
- if (next !== "done" && !Object.hasOwn(decl.steps, next)) {
853
- throw new TypeError(`step '${stepName}' returned unknown next step '${next}'`);
854
- }
855
- const state = decl.state.parse((result as { state: unknown }).state) as JsonValue;
856
-
857
- const sleepUntilRaw = (result as { sleepUntil?: unknown }).sleepUntil;
858
- const waitForRaw = (result as { waitFor?: unknown }).waitFor;
859
- if (sleepUntilRaw !== undefined && waitForRaw !== undefined) {
860
- throw new TypeError(`step '${stepName}' cannot return both sleepUntil and waitFor`);
861
- }
862
- let sleepUntil: number | undefined;
863
- if (sleepUntilRaw !== undefined) {
864
- if (typeof sleepUntilRaw !== "number" || !Number.isFinite(sleepUntilRaw)) {
865
- throw new TypeError(`step '${stepName}' sleepUntil must be a finite number`);
866
- }
867
- sleepUntil = sleepUntilRaw;
868
- }
869
- let waitFor: WaitCondition | undefined;
870
- if (waitForRaw !== undefined) {
871
- waitFor = this.#validateWaitCondition(stepName, waitForRaw);
872
- }
873
- if (next === "done" && (sleepUntil !== undefined || waitFor !== undefined)) {
874
- throw new TypeError(`step '${stepName}' cannot schedule work after next=done`);
875
- }
876
-
877
- const hasOutput = next === "done" && Object.hasOwn(result, "output");
878
- const nextStatus: WorkflowStatusName = next === "done" ? "done"
879
- : sleepUntil !== undefined ? "sleeping"
880
- : waitFor !== undefined ? "waiting"
881
- : "running";
882
- return {
883
- state,
884
- next,
885
- nextStatus,
886
- ...(sleepUntil !== undefined ? { sleepUntil } : {}),
887
- ...(waitFor !== undefined ? { waitFor } : {}),
888
- ...(hasOutput ? { output: (result as { output?: unknown }).output } : {}),
889
- hasOutput,
890
- };
891
- }
892
-
893
- #validateWaitCondition(stepName: string, value: unknown): WaitCondition {
894
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
895
- throw new TypeError(`step '${stepName}' waitFor must be a WaitCondition object`);
896
- }
897
- const kind = (value as { kind?: unknown }).kind;
898
- if (kind === "event") {
899
- const topic = (value as { topic?: unknown }).topic;
900
- if (typeof topic !== "string" || !topic.includes(".")) {
901
- throw new TypeError(`step '${stepName}' waitFor event requires a '<module>.<name>' topic`);
902
- }
903
- const correlationId = (value as { correlationId?: unknown }).correlationId;
904
- return {
905
- kind: "event",
906
- topic: topic as `${string}.${string}`,
907
- ...(correlationId !== undefined ? { correlationId: String(correlationId) } : {}),
908
- };
909
- }
910
- if (kind === "command") {
911
- const commandId = (value as { commandId?: unknown }).commandId;
912
- if (typeof commandId !== "string" || commandId.length === 0) {
913
- throw new TypeError(`step '${stepName}' waitFor command requires a commandId`);
914
- }
915
- return { kind: "command", commandId };
916
- }
917
- throw new TypeError(`step '${stepName}' waitFor kind must be 'event' or 'command'`);
918
- }
919
-
920
- /** Durably transition an open instance to `failed`. M1 has no retry driver (that is M3), so an
921
- * unhandled step throw or an invalid result terminates the instance rather than looping. */
922
- async #fail(instanceId: string, message: string): Promise<void> {
923
- const requestKey = this.#deps.rng.uuid(), now = this.#deps.clock.now();
924
- await recoverIndeterminate(() => this.#deps.persistence.controlWorkflow(requestKey, "fail", instanceId, now, { error: message }));
925
- }
926
-
927
- async #renew(instanceId: string, token: string): Promise<boolean> {
928
- const requestKey = this.#deps.rng.uuid();
929
- const now = this.#deps.clock.now();
930
- return recoverIndeterminate(() => this.#deps.persistence.renewWorkflowLease(requestKey, { instanceId, token, now, ttlMs: this.#leaseTtlMs }));
931
- }
932
- }
933
-
934
- export function createWorkflowRuntime(deps: WorkflowRuntimeDeps): WorkflowRuntime {
935
- return new WorkflowRuntime(deps);
936
- }
937
-
938
- export type { WorkflowResumePoint };