@humwick/server 0.130.2 → 0.130.4

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 (587) hide show
  1. package/bundled/prompts/manifest.json +1 -0
  2. package/bundled/prompts/task.hydration.md +1 -1
  3. package/bundled/roles/builder.md +1 -1
  4. package/docs/guides/sub-coordinators.md +12 -5
  5. package/docs/guides/tools-coordination.md +1 -0
  6. package/docs/guides/workflows.md +16 -0
  7. package/docs/openapi.json +175 -4
  8. package/package.json +8 -7
  9. package/public/assets/{MessageBubble-BWLqMGKG.js → MessageBubble-BOkmEvgZ.js} +2 -2
  10. package/public/assets/{PlanGraphPanel-3TlIQpjk.js → PlanGraphPanel-BkaDJp0r.js} +2 -2
  11. package/public/assets/{TraceView-Wimm0Czv.js → TraceView-B99Uu2yT.js} +2 -2
  12. package/public/assets/{VaultDecisionCard-Bau_vNax.js → VaultDecisionCard-DUIfTTFF.js} +2 -2
  13. package/public/assets/{activity-CrSurn-r.js → activity-ZIX0N1El.js} +2 -2
  14. package/public/assets/{agent-lineage-CknAJKcb.js → agent-lineage-kq9TPYFw.js} +2 -2
  15. package/public/assets/agent-liveness-DSQY1Aa_.js +2 -0
  16. package/public/assets/{agent-name-DKbiIhBo.js → agent-name-C_7F4DFR.js} +2 -2
  17. package/public/assets/{agent-profiles-DwF6EQ4R.js → agent-profiles-BNOcrGcY.js} +2 -2
  18. package/public/assets/{agent-project-badge-Diz4Sjjb.js → agent-project-badge-B6yAh70a.js} +2 -2
  19. package/public/assets/{agent-tools-CRhpRQza.js → agent-tools-CcrhmOzF.js} +2 -2
  20. package/public/assets/{agent-type-icon-LrUEU3lB.js → agent-type-icon-CXSLrKiq.js} +2 -2
  21. package/public/assets/{agents-B-v66C-4.js → agents-DIZG97_X.js} +2 -2
  22. package/public/assets/{analytics-BqCb-LKi.js → analytics-rasmoPjR.js} +2 -2
  23. package/public/assets/{artifact-viewer-kf4pGuAR.js → artifact-viewer-tBpTrEPb.js} +2 -2
  24. package/public/assets/{artifacts-DJevXcvg.js → artifacts-BNkrynP1.js} +2 -2
  25. package/public/assets/{artifacts-D32zyw4Y.js → artifacts-Bjq-ZyBA.js} +2 -2
  26. package/public/assets/{automation-BpRajx_5.js → automation-Cx422L3L.js} +2 -2
  27. package/public/assets/{backlog-D_UjTdzX.js → backlog-RuFQ8PUu.js} +2 -2
  28. package/public/assets/{busy-health-N3VEh4ao.js → busy-health-BvNQ9g5x.js} +2 -2
  29. package/public/assets/{channels-9jopungq.js → channels-DNbKAS0I.js} +2 -2
  30. package/public/assets/{chat-CSXrTB3C.js → chat-Bp3Fx73h.js} +3 -3
  31. package/public/assets/{code-preview-p1NnmqK0.js → code-preview-KIRZA6cz.js} +2 -2
  32. package/public/assets/{connectors-CZdqlDNL.js → connectors-k7vO4vvc.js} +2 -2
  33. package/public/assets/{cosmos-BgdsE4dG.js → cosmos-BuVoq1aw.js} +2 -2
  34. package/public/assets/{crate-agent-30aXTKva.js → crate-agent-irqlt4NH.js} +2 -2
  35. package/public/assets/{dependency-graph-B_jvw2GE.js → dependency-graph-D_rurNBz.js} +2 -2
  36. package/public/assets/{deploys-DoN6bAb5.js → deploys-yzZqIpxE.js} +2 -2
  37. package/public/assets/{display-BrKWFGJz.js → display-BemJG1f_.js} +2 -2
  38. package/public/assets/{files-D6XEu8Ng.js → files-DXWcrxOz.js} +2 -2
  39. package/public/assets/{fleet-C7jQWbw2.js → fleet-TEuOCZXE.js} +2 -2
  40. package/public/assets/{fleet-metrics-BiDs3MLx.js → fleet-metrics-BGoDAFI-.js} +2 -2
  41. package/public/assets/{formatted-body-impl-DyG0WsQS.js → formatted-body-impl-BB9eq9nN.js} +2 -2
  42. package/public/assets/{formatted-body-CQBwc1fb.js → formatted-body-qy2nTD_1.js} +3 -3
  43. package/public/assets/{host-providers-dialog-DKaW31dN.js → host-providers-dialog-BGPHxuwT.js} +2 -2
  44. package/public/assets/{index-CRHep3M9.js → index-BC5XZc0z.js} +5 -5
  45. package/public/assets/{index-MYAIwYTX.css → index-D8Rp9pz2.css} +1 -1
  46. package/public/assets/{insights-CzrtkpOE.js → insights-C-KQG0h-.js} +2 -2
  47. package/public/assets/{integrations-aVsUmigt.js → integrations-ClPRPyKU.js} +2 -2
  48. package/public/assets/{issue-ref-chip-BnSlM_95.js → issue-ref-chip-DbH8J9Ak.js} +2 -2
  49. package/public/assets/{log-viewer-qe7Ee47Z.js → log-viewer-PH4zVn24.js} +2 -2
  50. package/public/assets/{maintenance-NfjqFZAk.js → maintenance-5lV43uTL.js} +2 -2
  51. package/public/assets/{managed-agents-BiH0HgaE.js → managed-agents-BdgU35aX.js} +2 -2
  52. package/public/assets/{markdown-preview-BixaT3Xr.js → markdown-preview-DNT4OUwQ.js} +3 -3
  53. package/public/assets/{markdown-preview-impl-DNGlWIS3.js → markdown-preview-impl-CnCcPy0A.js} +2 -2
  54. package/public/assets/{memory-DNsCl6UX.js → memory--GgWH1gH.js} +2 -2
  55. package/public/assets/{messages-DopU1L1X.js → messages-CzFl_97O.js} +2 -2
  56. package/public/assets/{orchestrators-Cq7ckLQT.js → orchestrators-CVvJVkzv.js} +2 -2
  57. package/public/assets/overview-DsrZvE86.js +2 -0
  58. package/public/assets/{pairs-DE_m6rrE.js → pairs-D2d0d2py.js} +2 -2
  59. package/public/assets/{profile-icon-B3rfh77F.js → profile-icon-_SMuSbYF.js} +2 -2
  60. package/public/assets/{project-profile-instructions-DQWZE0kA.js → project-profile-instructions-Apn15y43.js} +2 -2
  61. package/public/assets/{projects-Br2Bd2h1.js → projects-D3B6kto5.js} +2 -2
  62. package/public/assets/{prompt-settings-BKvFsvGD.js → prompt-settings-DnPB6Ez6.js} +2 -2
  63. package/public/assets/provider-display-bWwiuedV.js +2 -0
  64. package/public/assets/{registry-Cw38_6gW.js → registry-D790cIhN.js} +2 -2
  65. package/public/assets/run-card-BCro-Odj.js +2 -0
  66. package/public/assets/{secrets-DxI-vzSq.js → secrets-CVXvfvbU.js} +2 -2
  67. package/public/assets/{service-access-settings-D9s_VJbd.js → service-access-settings-D3Xxqb9w.js} +2 -2
  68. package/public/assets/{settings-CKZTVGIx.js → settings-CoUC7Jci.js} +2 -2
  69. package/public/assets/shipping-D_Lp9n_J.js +2 -0
  70. package/public/assets/{stats-DyUBKyWI.js → stats-CXiNjB9S.js} +2 -2
  71. package/public/assets/{status-dot-DYqlEUhJ.js → status-dot-C703YIy1.js} +2 -2
  72. package/public/assets/store-BgDALGIX.js +9 -0
  73. package/public/assets/{tasks-Bh7Chr4-.js → tasks-DO0WpJCx.js} +2 -2
  74. package/public/assets/{teams-bowI7qVY.js → teams-FGkdi_tL.js} +2 -2
  75. package/public/assets/{terminal-viewer-BgVLHdsU.js → terminal-viewer-Dh8dSC7e.js} +3 -3
  76. package/public/assets/{terminal-viewer-impl-Dz22UPy6.js → terminal-viewer-impl-B5Zriip3.js} +2 -2
  77. package/public/assets/{use-agent-terminal-DSQQ-iY7.js → use-agent-terminal-CRUVDvLC.js} +2 -2
  78. package/public/assets/{use-decisions-samMQhQf.js → use-decisions-CoaqRh4l.js} +2 -2
  79. package/public/assets/{use-project-list-59LheVP7.js → use-project-list-X-BgT53X.js} +2 -2
  80. package/public/assets/{use-selectors-BZa25xEB.js → use-selectors-0X_UZkk-.js} +2 -2
  81. package/public/assets/{user-avatar-DHxlvUvV.js → user-avatar-CkLhVBGu.js} +2 -2
  82. package/public/assets/{versioned-prompt-editor-BmED5DnJ.js → versioned-prompt-editor-BkTi7eim.js} +2 -2
  83. package/public/assets/{work-queue-CSvpuNLz.js → work-queue-DDOLEyKo.js} +2 -2
  84. package/public/assets/{workflow-marker-DAZcj5qt.js → workflow-marker-CzOsB8BM.js} +2 -2
  85. package/public/assets/{workflows-DdmxRSyg.js → workflows-DJilvmRM.js} +2 -2
  86. package/public/assets/{workspaces-BxeKfVuF.js → workspaces-CHcfbLR3.js} +2 -2
  87. package/public/cosmos.html +4 -4
  88. package/public/index.html +29 -29
  89. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  90. package/runner/src/adapters/claude-api.ts +1 -1
  91. package/runner/src/adapters/claude-headless.ts +1 -1
  92. package/runner/src/adapters/claude-launch-exit.ts +26 -0
  93. package/runner/src/adapters/claude-session-probe.ts +1 -1
  94. package/runner/src/adapters/claude-tmux.ts +38 -10
  95. package/runner/src/adapters/claude.ts +28 -75
  96. package/runner/src/adapters/provider-quota-harvest.ts +20 -71
  97. package/runner/src/agent-runner/lifecycle-milestones.ts +2 -2
  98. package/runner/src/agent-runner/liveness-exchange.ts +104 -0
  99. package/runner/src/claude-system-prompt.ts +49 -0
  100. package/runner/src/launch-assembly.ts +14 -10
  101. package/runner/src/profile-home.ts +1 -1
  102. package/runner/src/project-instructions.ts +2 -2
  103. package/runner/src/provider-command.ts +18 -0
  104. package/runner/src/provider-setup.ts +3 -2
  105. package/runner/src/runner-core.ts +14 -15
  106. package/runner/src/runner-helpers.ts +0 -8
  107. package/runner/src/session-scratch.ts +6 -2
  108. package/runner/test-support/provisioning-context.ts +4 -2
  109. package/runner/test-support/runner-harness.ts +2 -1
  110. package/scripts/steward-policy-cleanup.ts +2 -2
  111. package/src/agent-authz.ts +2 -2
  112. package/src/agent-drive-arbiter.ts +3 -8
  113. package/src/agent-exit-cleanup-async.ts +2 -2
  114. package/src/agent-exit-cleanup.ts +23 -19
  115. package/src/agent-lifecycle-events.ts +112 -85
  116. package/src/agent-ref.ts +3 -2
  117. package/src/agent-status-events.ts +43 -14
  118. package/src/auth/login-link.ts +35 -0
  119. package/src/auth/password.ts +4 -3
  120. package/src/auth/session-refresh.ts +1 -1
  121. package/src/auth/session.test-fixture.ts +2 -2
  122. package/src/auth/session.ts +7 -7
  123. package/src/automations/crud.ts +8 -81
  124. package/src/automations/dispatch.ts +33 -58
  125. package/src/automations/index.ts +1 -1
  126. package/src/automations/persistence.ts +65 -0
  127. package/src/automations/precheck.ts +18 -16
  128. package/src/automations/read-async.ts +11 -0
  129. package/src/automations/reconcile.ts +30 -39
  130. package/src/automations/storage.ts +33 -10
  131. package/src/automations/write-async.ts +33 -0
  132. package/src/branch-land-notice.ts +3 -3
  133. package/src/branch-landed.ts +25 -23
  134. package/src/bus.ts +21 -19
  135. package/src/cd-deploy-notify.ts +2 -2
  136. package/src/ci/notifications.ts +4 -4
  137. package/src/ci/root-missing.ts +3 -3
  138. package/src/ci/transition-notify.ts +5 -5
  139. package/src/claude-app/oauth-http.ts +3 -3
  140. package/src/cli/index.ts +7 -1
  141. package/src/cli/init.ts +53 -0
  142. package/src/cli/orchestrator.ts +1 -7
  143. package/src/cli/setup.ts +6 -4
  144. package/src/cli/workflow.ts +12 -3
  145. package/src/commands-db.ts +4 -4
  146. package/src/commands-write.ts +8 -0
  147. package/src/compaction-watch.ts +10 -8
  148. package/src/config-store-write.ts +2 -2
  149. package/src/config.ts +4 -11
  150. package/src/connectors.ts +10 -8
  151. package/src/context-advisory.ts +22 -21
  152. package/src/db/agent-action-write.ts +36 -0
  153. package/src/db/agent-control-async.ts +9 -0
  154. package/src/db/agent-control-write.ts +49 -0
  155. package/src/db/agent-effort-observe.ts +22 -0
  156. package/src/db/agent-meta.ts +14 -3
  157. package/src/db/agents-async.ts +8 -5
  158. package/src/db/agents.ts +15 -12
  159. package/src/db/auth-identities.ts +2 -2
  160. package/src/db/automation-read-ops.ts +11 -0
  161. package/src/db/automation-write-ops.ts +46 -0
  162. package/src/db/blackboard.ts +2 -2
  163. package/src/db/browser-session-write.ts +32 -0
  164. package/src/db/bus-outbox-write-ops.ts +3 -1
  165. package/src/db/checkout-refresh-schema.ts +47 -0
  166. package/src/db/checkout-refresh.ts +5 -5
  167. package/src/db/command-agent-profile-backfill.ts +39 -0
  168. package/src/db/command-agent-profiles.ts +5 -1
  169. package/src/db/command-retention.ts +126 -0
  170. package/src/db/commands-planner-stats.test-fixture.ts +8 -0
  171. package/src/db/commands-read-ops.ts +16 -0
  172. package/src/db/connection.ts +1 -182
  173. package/src/db/context-carry-write.ts +74 -0
  174. package/src/db/continuations.ts +4 -4
  175. package/src/db/db-error-registry.ts +12 -0
  176. package/src/db/gateway.ts +26 -133
  177. package/src/db/history-retention-ops.ts +13 -0
  178. package/src/db/inbox-async.ts +2 -2
  179. package/src/db/inbox-read-ops.ts +1 -1
  180. package/src/db/inbox.ts +9 -3
  181. package/src/db/issue-associations.ts +2 -2
  182. package/src/db/issue-refs-async.ts +4 -1
  183. package/src/db/issue-refs-read-ops.ts +6 -0
  184. package/src/db/issue-refs.ts +8 -8
  185. package/src/db/kernel-event-log-schema.ts +2 -0
  186. package/src/db/land-boundaries.ts +51 -5
  187. package/src/db/land-gate-attestations.ts +2 -2
  188. package/src/db/landing-receipts.ts +2 -2
  189. package/src/db/lifecycle-context-write.ts +5 -0
  190. package/src/db/lifecycle-tail-async.ts +76 -0
  191. package/src/db/lifecycle-tail-write-ops.ts +62 -0
  192. package/src/db/lifecycle-write-window.ts +2 -1
  193. package/src/db/lineage-pair-dedupe-sql.ts +3 -3
  194. package/src/db/main-connection-mode.ts +11 -0
  195. package/src/db/main-connection-writes.ts +20 -2
  196. package/src/db/maintenance-job-runs-async.ts +2 -2
  197. package/src/db/maintenance-job-write-ops.ts +1 -1
  198. package/src/db/maintenance-pauses.ts +2 -2
  199. package/src/db/message-send.ts +4 -0
  200. package/src/db/messages.ts +31 -25
  201. package/src/db/migrations.ts +18 -1
  202. package/src/db/notification-write-ops.ts +19 -0
  203. package/src/db/operator-answer-write.ts +19 -0
  204. package/src/db/orchestrators.ts +10 -4
  205. package/src/db/pairs-async.ts +22 -1
  206. package/src/db/pairs-write-ops.ts +8 -3
  207. package/src/db/pairs.ts +41 -48
  208. package/src/db/plan-dispatch-schema.ts +5 -1
  209. package/src/db/plan-ready-notices.ts +43 -0
  210. package/src/db/plan-tools-include-migration.ts +36 -0
  211. package/src/db/plans-write-ops.ts +2 -0
  212. package/src/db/presence-buffer.ts +5 -0
  213. package/src/db/presence-loop.ts +18 -23
  214. package/src/db/presence-write.ts +0 -6
  215. package/src/db/project-entries-async.ts +14 -0
  216. package/src/db/project-entries-read-ops.ts +5 -1
  217. package/src/db/project-entries-write-ops.ts +8 -0
  218. package/src/db/project-entries.ts +15 -12
  219. package/src/db/project-journal.ts +2 -2
  220. package/src/db/project-trust-approvals.ts +2 -2
  221. package/src/db/prompt-templates.ts +4 -4
  222. package/src/db/read-op-marshaling-fixtures.ts +18 -0
  223. package/src/db/read-ops.ts +4 -0
  224. package/src/db/retention.ts +13 -2
  225. package/src/db/review-publish-write.ts +8 -0
  226. package/src/db/runtime-admin-async.ts +19 -0
  227. package/src/db/runtime-admin-write-ops.ts +36 -0
  228. package/src/db/runtime-tail-async.ts +288 -0
  229. package/src/db/runtime-tail-write-ops.ts +97 -0
  230. package/src/db/schema.ts +4 -1
  231. package/src/db/spawn-budget.ts +5 -0
  232. package/src/db/spawn-delegation.ts +3 -12
  233. package/src/db/sql-audit-indexes.ts +3 -0
  234. package/src/db/stats-async.ts +5 -3
  235. package/src/db/task-entity.ts +5 -4
  236. package/src/db/team-claim-policy.ts +60 -0
  237. package/src/db/teams-async.ts +4 -0
  238. package/src/db/teams-write-ops.ts +4 -0
  239. package/src/db/terminal-events.ts +15 -0
  240. package/src/db/timer-cutover-bodies.ts +292 -0
  241. package/src/db/timer-cutover-core.ts +37 -278
  242. package/src/db/timer-cutover-state.ts +40 -0
  243. package/src/db/timer-cutover-write-ops.ts +1 -40
  244. package/src/db/token-db-write-ops.ts +2 -0
  245. package/src/db/token-standing-async.ts +12 -0
  246. package/src/db/token-standing.ts +6 -9
  247. package/src/db/token-usage-retention.ts +33 -0
  248. package/src/db/token-usage-write-ops.ts +1 -1
  249. package/src/db/token-usage.ts +4 -1
  250. package/src/db/user-invites-async.ts +13 -0
  251. package/src/db/user-invites.ts +3 -3
  252. package/src/db/users.ts +14 -6
  253. package/src/db/workflow-execution.ts +3 -3
  254. package/src/db/workspaces.ts +8 -5
  255. package/src/db/write-ops.ts +31 -12
  256. package/src/db/writer-fault-seam.ts +2 -9
  257. package/src/db/writer-fault-test-ops.ts +18 -0
  258. package/src/db/writer-gateway.ts +42 -46
  259. package/src/dev.ts +12 -16
  260. package/src/event-loop-lag.ts +4 -2
  261. package/src/event-loop-yield.ts +2 -2
  262. package/src/events.ts +10 -8
  263. package/src/host-provider-store.ts +5 -4
  264. package/src/hub-metrics-report.ts +5 -1
  265. package/src/hub-metrics.ts +38 -18
  266. package/src/init.ts +299 -0
  267. package/src/injection-sweep-pacing.ts +6 -8
  268. package/src/invite-link.ts +16 -9
  269. package/src/land-gate-notifications.ts +11 -10
  270. package/src/land-gate-queue-progress.ts +3 -3
  271. package/src/land-settle-dispatch.ts +52 -92
  272. package/src/lifecycle-context-pressure.ts +11 -10
  273. package/src/lifecycle-manager.ts +104 -98
  274. package/src/maintenance/breaker.ts +18 -16
  275. package/src/maintenance/checkout-refresh.ts +18 -1
  276. package/src/maintenance/critical-job-watchdog.ts +3 -3
  277. package/src/maintenance/events.ts +3 -3
  278. package/src/maintenance/history-retention-job.ts +30 -0
  279. package/src/maintenance/jobs.ts +29 -67
  280. package/src/maintenance/long-running-work.ts +3 -1
  281. package/src/maintenance/offline-agent-prune-job.ts +28 -0
  282. package/src/maintenance/orphaned-sessions.ts +5 -4
  283. package/src/maintenance/pauses.ts +4 -10
  284. package/src/maintenance/plan-ready-notice-job.ts +71 -0
  285. package/src/maintenance/scheduler.ts +18 -13
  286. package/src/maintenance/stalled-delivery-pager.ts +4 -4
  287. package/src/maintenance/teams.ts +19 -30
  288. package/src/maintenance/workspaces.ts +17 -27
  289. package/src/managed-policy-escalation.ts +7 -7
  290. package/src/managed-policy.ts +3 -3
  291. package/src/managed-provider-gate.ts +5 -4
  292. package/src/managed-spawn-reservation.ts +5 -4
  293. package/src/mcp/index.ts +8 -8
  294. package/src/mcp/output-descriptions.ts +1 -1
  295. package/src/mcp/presentation.ts +2 -1
  296. package/src/mcp/runtime-output.ts +6 -1
  297. package/src/mcp/self-care-visibility.ts +2 -2
  298. package/src/mcp/spawn-targets-matrix.ts +10 -1
  299. package/src/mcp/tool-groups.ts +3 -2
  300. package/src/mcp/tool-roles.ts +81 -0
  301. package/src/mcp/tools-workspace.ts +1 -1
  302. package/src/mcp-blackboard-tools.ts +6 -5
  303. package/src/mcp-decision-tools.ts +7 -7
  304. package/src/mcp-egress-tools.ts +10 -9
  305. package/src/mcp-issue-activity-tools.ts +2 -2
  306. package/src/mcp-issue-association-tools.ts +4 -4
  307. package/src/mcp-issue-ref-tools.ts +3 -3
  308. package/src/mcp-merge-tools.ts +5 -5
  309. package/src/mcp-notification-subscription-tools.ts +14 -15
  310. package/src/mcp-output-levels.test-fixture.ts +1 -1
  311. package/src/mcp-profile-trust-tools.ts +1 -1
  312. package/src/mcp-project-journal-tools.ts +4 -12
  313. package/src/mcp-project-tools.ts +17 -18
  314. package/src/mcp-routing-policy-tools.ts +3 -2
  315. package/src/mcp-team-tools.ts +6 -6
  316. package/src/mcp-workflow-tools.ts +5 -3
  317. package/src/memory-area-injection.ts +13 -12
  318. package/src/memory-boundary-capture.ts +6 -6
  319. package/src/memory-injection.ts +45 -43
  320. package/src/merge-pause-maintenance.ts +5 -4
  321. package/src/notification-router.ts +44 -17
  322. package/src/notify.ts +5 -5
  323. package/src/observed-reset-refresh.ts +4 -4
  324. package/src/operator-alarm.ts +2 -5
  325. package/src/project-ingest.ts +6 -6
  326. package/src/project-instructions.ts +8 -1
  327. package/src/project-trust.ts +12 -11
  328. package/src/prompt-resolver.ts +3 -3
  329. package/src/provider-quota-async.ts +5 -7
  330. package/src/provider-timeline-event.ts +4 -7
  331. package/src/quota-band-delivery.ts +2 -2
  332. package/src/recipe-runner.ts +4 -3
  333. package/src/retention-config-store.ts +4 -3
  334. package/src/reviewer-pipeline.ts +11 -10
  335. package/src/routes/_route.ts +4 -4
  336. package/src/routes/_shared.ts +13 -12
  337. package/src/routes/agent-effort.ts +5 -5
  338. package/src/routes/agent-heartbeat-quota.ts +23 -0
  339. package/src/routes/agent-keep.ts +1 -1
  340. package/src/routes/agent-reporting.ts +1 -1
  341. package/src/routes/agent-self-effort.ts +5 -5
  342. package/src/routes/agent-sessions.ts +24 -24
  343. package/src/routes/agents-spawn.ts +1 -1
  344. package/src/routes/agents.ts +17 -119
  345. package/src/routes/auth.ts +44 -17
  346. package/src/routes/authority-audit.ts +2 -2
  347. package/src/routes/automations.ts +6 -5
  348. package/src/routes/channel-binding-delete.ts +1 -1
  349. package/src/routes/channel-output.ts +3 -3
  350. package/src/routes/claude-api-budget.ts +5 -5
  351. package/src/routes/commands-listing.ts +1 -1
  352. package/src/routes/commands.ts +29 -27
  353. package/src/routes/context-resets.ts +2 -2
  354. package/src/routes/coordinator-teams.ts +1 -1
  355. package/src/routes/decisions.ts +2 -2
  356. package/src/routes/host-providers.ts +18 -0
  357. package/src/routes/host-secret-files.ts +1 -1
  358. package/src/routes/hosts.ts +11 -11
  359. package/src/routes/inbox.ts +3 -2
  360. package/src/routes/index.ts +7 -3
  361. package/src/routes/integrations.ts +9 -7
  362. package/src/routes/issue-activity.ts +2 -2
  363. package/src/routes/issue-associations.ts +4 -4
  364. package/src/routes/issue-refs.ts +3 -3
  365. package/src/routes/kernel-timers-operator.ts +1 -1
  366. package/src/routes/memory.ts +1 -1
  367. package/src/routes/merges.ts +11 -11
  368. package/src/routes/messages.ts +23 -17
  369. package/src/routes/orchestrator-bootstrap.ts +3 -1
  370. package/src/routes/orchestrator-host-registration.ts +2 -0
  371. package/src/routes/orchestrator-host-utilization.ts +2 -2
  372. package/src/routes/orchestrator-proxy.ts +2 -2
  373. package/src/routes/orchestrator.ts +27 -18
  374. package/src/routes/pairs.ts +8 -8
  375. package/src/routes/project-profile-instructions.ts +1 -1
  376. package/src/routes/project-trust.ts +1 -1
  377. package/src/routes/projects.ts +16 -16
  378. package/src/routes/prompt-templates.ts +12 -12
  379. package/src/routes/provider-quotas.ts +10 -10
  380. package/src/routes/run-command.ts +1 -1
  381. package/src/routes/secrets-backup.ts +4 -5
  382. package/src/routes/secrets-import.ts +3 -4
  383. package/src/routes/secrets-vault.ts +12 -14
  384. package/src/routes/settings.ts +1 -1
  385. package/src/routes/shipping.ts +9 -1
  386. package/src/routes/spawn-policy.ts +8 -7
  387. package/src/routes/sse.ts +2 -2
  388. package/src/routes/team-templates.ts +7 -7
  389. package/src/routes/teams.ts +10 -10
  390. package/src/routes/tokens.ts +18 -17
  391. package/src/routes/users.ts +24 -24
  392. package/src/routes/viewer-read.ts +3 -3
  393. package/src/routes/web-tool-calls.ts +4 -4
  394. package/src/routes/workflows.ts +2 -2
  395. package/src/routes/workspaces.ts +17 -16
  396. package/src/routing-policy-push.ts +6 -6
  397. package/src/runtime-grant.ts +17 -11
  398. package/src/runtime-install.ts +30 -0
  399. package/src/runtime-tokens.ts +20 -28
  400. package/src/secrets-broker/config.ts +1 -1
  401. package/src/secrets-broker/service.ts +2 -2
  402. package/src/security.ts +3 -1
  403. package/src/self-resume-carry.ts +21 -110
  404. package/src/self-resume-supersede.ts +4 -3
  405. package/src/self-resume.ts +54 -66
  406. package/src/server-background.ts +1 -1
  407. package/src/server.ts +25 -26
  408. package/src/services/agent-action.ts +12 -31
  409. package/src/services/agent-effort.ts +7 -32
  410. package/src/services/agent-hang-watchdog-job.ts +2 -2
  411. package/src/services/agent-heartbeat.ts +166 -0
  412. package/src/services/agent-reporting.ts +1 -1
  413. package/src/services/auth-context.ts +6 -0
  414. package/src/services/browser-inventory.ts +3 -3
  415. package/src/services/browser-session-state.ts +3 -2
  416. package/src/services/browser-session.ts +21 -27
  417. package/src/services/bus-command-update.ts +9 -9
  418. package/src/services/checkout-refresh-state.ts +1 -0
  419. package/src/services/checkout-refresh.test-fixture.ts +3 -2
  420. package/src/services/command-host.ts +7 -2
  421. package/src/services/coordinator-digest.ts +3 -2
  422. package/src/services/dispatch-command.ts +12 -13
  423. package/src/services/effective-launch-driver.ts +14 -9
  424. package/src/services/host-changed-paths.test-fixture.ts +5 -6
  425. package/src/services/host-project-readiness.ts +2 -2
  426. package/src/services/host-providers.ts +1 -1
  427. package/src/services/host-utilization.ts +9 -9
  428. package/src/services/idle-agent-digest.ts +17 -12
  429. package/src/services/idle-epics.ts +4 -4
  430. package/src/services/interrupt-agent.ts +12 -9
  431. package/src/services/issue-activity.ts +3 -3
  432. package/src/services/issue-associations.ts +15 -18
  433. package/src/services/issue-lifecycle-comments.ts +3 -2
  434. package/src/services/issue-lifecycle.ts +14 -13
  435. package/src/services/issue-refs.ts +4 -5
  436. package/src/services/kernel-timers-cutover-chaos-worker.ts +1 -1
  437. package/src/services/kernel-timers-ingress-chaos-worker.ts +6 -1
  438. package/src/services/kernel-timers-ingress.ts +3 -3
  439. package/src/services/kernel-timers-message-authority-chaos-worker.ts +3 -10
  440. package/src/services/kernel-timers-operator-chaos-worker.ts +3 -2
  441. package/src/services/kernel-timers-outbox.ts +1 -1
  442. package/src/services/kernel-timers-stage10-operator.ts +1 -1
  443. package/src/services/kernel-timers.ts +4 -3
  444. package/src/services/land-gate-placement.ts +11 -2
  445. package/src/services/land-issue-relations.ts +5 -4
  446. package/src/services/land-restart-retry.ts +7 -6
  447. package/src/services/landed-ancestry.ts +6 -5
  448. package/src/services/legacy-landing-host.ts +4 -1
  449. package/src/services/legacy-landing-recovery.ts +8 -6
  450. package/src/services/managed-running.ts +17 -17
  451. package/src/services/memory-areas.ts +3 -3
  452. package/src/services/merge-pause.ts +9 -15
  453. package/src/services/operator-decisions.ts +37 -41
  454. package/src/services/orchestrator-registration-recovery.ts +6 -2
  455. package/src/services/orchestrator-upgrade.ts +14 -12
  456. package/src/services/permission-decision.ts +4 -3
  457. package/src/services/plan-ci-notice.ts +1 -1
  458. package/src/services/plan-conformance.ts +4 -4
  459. package/src/services/plan-dispatch-policy.ts +10 -3
  460. package/src/services/plan-dispatch.ts +35 -14
  461. package/src/services/plan-graph.ts +39 -261
  462. package/src/services/plan-import.ts +1 -1
  463. package/src/services/plan-live-bindings.ts +8 -37
  464. package/src/services/plan-task-create.ts +1 -1
  465. package/src/services/plan-terminal-refs.ts +2 -12
  466. package/src/services/plan-wake-digest.ts +8 -3
  467. package/src/services/profile-trust.ts +2 -2
  468. package/src/services/project.ts +11 -10
  469. package/src/services/prompt-templates.ts +6 -5
  470. package/src/services/provider-api-key.ts +73 -0
  471. package/src/services/provider-hold-nudge.ts +9 -9
  472. package/src/services/provider-hold-record.ts +13 -12
  473. package/src/services/provider-state-audit.ts +19 -18
  474. package/src/services/provisioning-harness/driver.ts +1 -1
  475. package/src/services/provisioning-harness/fixture-matrix.ts +3 -3
  476. package/src/services/provisioning-harness/lifecycle.ts +24 -22
  477. package/src/services/provisioning-harness/prepare.ts +16 -17
  478. package/src/services/rate-limit-resume.ts +10 -9
  479. package/src/services/red-main-auto-revert.ts +7 -5
  480. package/src/services/red-main-controller-deps.ts +20 -3
  481. package/src/services/red-main-incident.ts +3 -1
  482. package/src/services/red-main-quarantine.ts +2 -3
  483. package/src/services/register-agent.ts +22 -22
  484. package/src/services/restart-project-role.ts +1 -1
  485. package/src/services/restart-spawn.ts +17 -16
  486. package/src/services/review-publish.ts +7 -24
  487. package/src/services/review-ref-reconcile.ts +29 -38
  488. package/src/services/runtime-dependency-alert.ts +27 -0
  489. package/src/services/scheduler-command.ts +11 -9
  490. package/src/services/scheduler.ts +5 -5
  491. package/src/services/self-effort-grant.ts +5 -25
  492. package/src/services/send-message.ts +16 -33
  493. package/src/services/shutdown-agent.ts +17 -14
  494. package/src/services/spawn-agent.ts +12 -41
  495. package/src/services/spawn-chain-inspector.ts +1 -1
  496. package/src/services/spawn-cwd-placement.ts +21 -5
  497. package/src/services/spawn-issue-activity.ts +8 -6
  498. package/src/services/spawn-launch.ts +17 -1
  499. package/src/services/spawn-loss-escalation.ts +1 -1
  500. package/src/services/spawn-registration-watchdog.ts +2 -2
  501. package/src/services/spawn-repo-probe.ts +7 -5
  502. package/src/services/spawn-tree-cleanup.ts +7 -8
  503. package/src/services/steward-reaper.ts +4 -4
  504. package/src/services/task-context-hydration.ts +25 -1
  505. package/src/services/task-delivery.ts +20 -34
  506. package/src/services/task-entity.ts +40 -203
  507. package/src/services/task-issue-link.ts +13 -20
  508. package/src/services/task-issue-weave.ts +12 -11
  509. package/src/services/task-lifecycle.ts +27 -31
  510. package/src/services/task-repo-cwd.ts +37 -0
  511. package/src/services/task-review-hold.ts +4 -17
  512. package/src/services/task-semantic-events.ts +13 -11
  513. package/src/services/task-state-reconcile.ts +3 -4
  514. package/src/services/task-validation.ts +2 -2
  515. package/src/services/team-templates.ts +5 -5
  516. package/src/services/team.ts +31 -98
  517. package/src/services/terminal-provider-exit.ts +6 -5
  518. package/src/services/timer-message-fire-chaos-worker.ts +7 -3
  519. package/src/services/timer-message-fire-outbox.ts +1 -1
  520. package/src/services/transient-agent-reaper.ts +22 -19
  521. package/src/services/turn-reconcile-audit.ts +9 -9
  522. package/src/services/user-onboarding.ts +8 -6
  523. package/src/services/user-profile.ts +6 -9
  524. package/src/services/work-finality.ts +16 -4
  525. package/src/services/workflow-auto-start.ts +2 -1
  526. package/src/services/workflow-caller.ts +61 -4
  527. package/src/services/workflow-child-proof-worker.ts +1 -1
  528. package/src/services/workflow-control-proof-worker.ts +1 -1
  529. package/src/services/workflow-coordinator.ts +8 -2
  530. package/src/services/workflow-decisions.ts +5 -5
  531. package/src/services/workflow-deferred-cleanup.ts +1 -1
  532. package/src/services/workflow-effect-driver.ts +42 -19
  533. package/src/services/workflow-effects.ts +41 -27
  534. package/src/services/workflow-pins.ts +7 -4
  535. package/src/services/workflow-reconcile.ts +1 -0
  536. package/src/services/workflow-resume-pins.ts +3 -3
  537. package/src/services/workflow-routing-defaults.ts +18 -0
  538. package/src/services/workflow-run-view.ts +4 -3
  539. package/src/services/workflow-run.test-fixture.ts +1 -1
  540. package/src/services/workflow-runs.ts +34 -22
  541. package/src/services/workflow-supervisor-state.ts +21 -1
  542. package/src/services/workflow-supervisor.ts +4 -3
  543. package/src/services/workspace-recovery.ts +4 -3
  544. package/src/setup.ts +47 -8
  545. package/src/spawn-admission.ts +10 -2
  546. package/src/spawn-placement.ts +27 -9
  547. package/src/spawn-targets.ts +8 -8
  548. package/src/sse-agent-status.ts +15 -6
  549. package/src/sse.ts +49 -24
  550. package/src/startup-context-injection.ts +61 -57
  551. package/src/steward-handoff.ts +4 -3
  552. package/src/steward-policy-cleanup.ts +9 -8
  553. package/src/steward.ts +2 -2
  554. package/src/task-dispatch-live.ts +5 -2
  555. package/src/task-dispatcher.ts +2 -2
  556. package/src/token-db.ts +2 -2
  557. package/src/token-usage-async.ts +2 -4
  558. package/src/user-awaiting-reply.ts +5 -5
  559. package/src/utils.ts +7 -0
  560. package/src/workspace-actions.ts +24 -29
  561. package/src/workspace-auto-merge.ts +14 -12
  562. package/src/workspace-changed-paths-client.ts +16 -5
  563. package/src/workspace-cleanup-stale.ts +30 -7
  564. package/src/workspace-command-settle.ts +24 -16
  565. package/src/workspace-config.ts +1 -1
  566. package/src/workspace-conflict-resolver.ts +9 -7
  567. package/src/workspace-dead-owner-recovery.ts +5 -4
  568. package/src/workspace-escalation.ts +12 -11
  569. package/src/workspace-human-escalation.ts +3 -3
  570. package/src/workspace-land-gate-watchdog.ts +3 -3
  571. package/src/workspace-land-refusal.ts +20 -0
  572. package/src/workspace-land-stopped.ts +1 -0
  573. package/src/workspace-merge.ts +10 -26
  574. package/src/workspace-nothing-to-land-notice.ts +2 -2
  575. package/src/workspace-orphans.ts +18 -37
  576. package/src/workspace-pr-completion.ts +10 -8
  577. package/src/workspace-rekey.ts +3 -3
  578. package/src/workspace-steward-failure.ts +2 -2
  579. package/src/workspace-steward-wake.ts +2 -2
  580. package/public/assets/agent-liveness-CaNwK0Zf.js +0 -2
  581. package/public/assets/overview-C1DCTzzV.js +0 -2
  582. package/public/assets/provider-display-DsuWMvVL.js +0 -2
  583. package/public/assets/run-card-CpKqRbhg.js +0 -2
  584. package/public/assets/shipping-B9YtnwtG.js +0 -2
  585. package/public/assets/store-DcClXLH0.js +0 -9
  586. package/runner/src/agent-runner/http-liveness.ts +0 -85
  587. package/src/db/on-loop-write-transaction.ts +0 -34
@@ -1,8 +1,9 @@
1
1
  import { stringValue } from "@humwick/sdk";
2
2
  import { childWorkflowsEnabled } from "../config";
3
3
  import { getAgentProfile } from "../config-store";
4
- import { getAgent, ValidationError } from "../db";
5
- import type { TaskDetail } from "../types";
4
+ import { activeTeamCoordinatorId, getAgent, ValidationError } from "../db";
5
+ import { taskOwnerRoleTeamId } from "../db/task-owner";
6
+ import type { AgentCard, TaskDetail } from "../types";
6
7
  import { ServiceAuthError } from "./errors";
7
8
  import type { AuthContext } from "./auth-context";
8
9
  import { canSpawn } from "./spawn-agent";
@@ -15,14 +16,70 @@ export function assertWorkflowCallerLive(agentId: string): void {
15
16
  if (workflowCallerGone(agentId)) throw new ValidationError("WORKFLOW_CALLER_GONE: caller is canceled or reaped");
16
17
  }
17
18
 
19
+ type WorkflowTaskSubject = Pick<TaskDetail, "createdBy" | "ownerAgentId" | "teamId" | "owner">;
20
+
21
+ /** An agent seated as its team's coordinator: it owns work and runs, it never builds a run's Task. */
22
+ export function isCoordinatorSeat(agent: Pick<AgentCard, "id" | "teamId"> | null | undefined): boolean {
23
+ return Boolean(agent?.teamId && activeTeamCoordinatorId(agent.teamId) === agent.id);
24
+ }
25
+
26
+ /** A spawned coordinator seat: a delegated sub-coordinator, never a worker it runs. */
27
+ function delegatedSeat(agentId: string | null | undefined): AgentCard | null {
28
+ const agent = agentId ? getAgent(agentId) : null;
29
+ return agent?.spawnedBy && isCoordinatorSeat(agent) ? agent : null;
30
+ }
31
+
32
+ /** Who may own a Task's runs (#3769): its assigned agent, its team's seat, its owner role's seat, its creator. */
33
+ function taskOwnerCandidates(task: WorkflowTaskSubject): Array<string | null | undefined> {
34
+ return [task.ownerAgentId, activeTeamCoordinatorId(task.teamId), activeTeamCoordinatorId(taskOwnerRoleTeamId(task.owner)), task.createdBy];
35
+ }
36
+
37
+ /** #3769 — the delegated sub-coordinator that owns a Task: the first candidate holding its private team's seat. */
38
+ export function delegatedTaskOwner(task: WorkflowTaskSubject): AgentCard | null {
39
+ for (const id of taskOwnerCandidates(task)) {
40
+ const seat = delegatedSeat(id);
41
+ if (seat) return seat;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /** #3818 — a Task its parent coordinator created (or holds the owner seat of) is the sub-coordinator's queue, no hand-over needed. */
47
+ function parentQueuedTask(seat: AgentCard, task: WorkflowTaskSubject): boolean {
48
+ const parent = seat.spawnedBy;
49
+ return Boolean(parent && (task.createdBy === parent || activeTeamCoordinatorId(taskOwnerRoleTeamId(task.owner)) === parent));
50
+ }
51
+
52
+ /** THE workflow-control predicate (start, read, resume, pause, cancel, budget): the Task's coordinators and admins, and
53
+ * a delegated sub-coordinator that created it, is assigned it, holds its team's seat or was spawned by its creator.
54
+ * A worker is never authorized. */
55
+ export function canControlWorkflowTask(ctx: AuthContext, task: WorkflowTaskSubject): boolean {
56
+ if (canControlTaskReviewHold(ctx, task)) return true;
57
+ const caller = ctx.callerAgentId;
58
+ const seat = delegatedSeat(caller);
59
+ return Boolean(seat && (taskOwnerCandidates(task).includes(seat.id) || parentQueuedTask(seat, task)));
60
+ }
61
+
18
62
  /** Workflow authority delegates execution to Relay, never the caller's direct-spawn capability. */
19
63
  export function admitWorkflowCaller(ctx: AuthContext, task: TaskDetail, workflow?: { name: string; source: string }) {
20
64
  const agent = ctx.callerAgentId ? getAgent(ctx.callerAgentId) : null;
21
65
  if (!agent || (!agent.spawnedBy && canSpawn(ctx))) return undefined;
22
66
  if (!childWorkflowsEnabled()) throw new ServiceAuthError("WORKFLOW_DISABLED: child workflows are disabled");
23
67
  assertWorkflowCallerLive(agent.id);
24
- if (task.ownerAgentId !== agent.id && task.createdBy !== agent.id && !canControlTaskReviewHold(ctx, task)) throw new ServiceAuthError("WORKFLOW_TASK_FORBIDDEN: caller must own or create the Task");
25
- const profile = getAgentProfile(ctx.constraints?.profile ?? stringValue(agent.meta?.profile) ?? "worker")?.value;
68
+ if (task.ownerAgentId !== agent.id && task.createdBy !== agent.id && !canControlWorkflowTask(ctx, task)) throw new ServiceAuthError("WORKFLOW_TASK_FORBIDDEN: caller must own or create the Task, or be a sub-coordinator of the coordinator that created it (hand one over with relay_task_assign_agent)");
69
+ return workflowCallerPolicy(agent, ctx.constraints?.profile, workflow);
70
+ }
71
+
72
+ /** #3769 — a run a Task coordinator starts on a Task a live delegated sub-coordinator owns is that owner's run:
73
+ * its spawn lineage, its quota and its notices, exactly as if the owner had started it. */
74
+ export function admitWorkflowTaskOwner(task: TaskDetail, workflow?: { name: string; source: string }) {
75
+ const owner = delegatedTaskOwner(task);
76
+ if (!owner || workflowCallerGone(owner.id)) return undefined;
77
+ if (!childWorkflowsEnabled()) throw new ServiceAuthError(`WORKFLOW_DISABLED: T${task.id} belongs to sub-coordinator ${owner.id}, and child workflows are disabled`);
78
+ return workflowCallerPolicy(owner, undefined, workflow);
79
+ }
80
+
81
+ function workflowCallerPolicy(agent: AgentCard, profileName: string | undefined, workflow?: { name: string; source: string }) {
82
+ const profile = getAgentProfile(profileName ?? stringValue(agent.meta?.profile) ?? "worker")?.value;
26
83
  const policy = profile?.workflows;
27
84
  const maxConcurrentRuns = policy?.maxConcurrentRuns ?? 1;
28
85
  const maxLiveAgents = policy?.maxLiveAgents ?? 2;
@@ -43,7 +43,7 @@ if (phase === "crash") {
43
43
  setWorkflowSupervisorTestOptions({ fault(label) {
44
44
  if (label === "after-child-complete") process.kill(process.pid, "SIGKILL");
45
45
  } });
46
- announceBranchLanded({ workspace: { id: WF.workspace, repoRoot: repo.cwd, branch: WF.branch, baseRef: "origin/main", ownerAgentId: WF.builder, orchestratorId: WF.orchestrator }, sourceHeadSha: sha, mergedSha: sha, pushed: true, taskIds: [task.id] });
46
+ await announceBranchLanded({ workspace: { id: WF.workspace, repoRoot: repo.cwd, branch: WF.branch, baseRef: "origin/main", ownerAgentId: WF.builder, orchestratorId: WF.orchestrator }, sourceHeadSha: sha, mergedSha: sha, pushed: true, taskIds: [task.id] });
47
47
  await workflowTaskDelivered();
48
48
  await settleChildProofStops();
49
49
  throw new Error("restart seam was not reached");
@@ -76,7 +76,7 @@ async function build(node: Node) {
76
76
  git(node.cwd!, "push", "-q", "origin", "HEAD:main");
77
77
  node.sha = sha; save();
78
78
  upsertWorkspace({ ...getWorkspace(node.workspace!)!, landedAt: Date.now(), landedSha: sha });
79
- announceBranchLanded({ workspace: { id: node.workspace!, repoRoot: node.cwd!, branch: node.branch!, baseRef: "main", ownerAgentId: node.agent!, orchestratorId: WF.orchestrator }, sourceHeadSha: sha, mergedSha: sha, pushed: true, taskIds: [node.taskId] });
79
+ await announceBranchLanded({ workspace: { id: node.workspace!, repoRoot: node.cwd!, branch: node.branch!, baseRef: "main", ownerAgentId: node.agent!, orchestratorId: WF.orchestrator }, sourceHeadSha: sha, mergedSha: sha, pushed: true, taskIds: [node.taskId] });
80
80
  await workflowTaskDelivered(); await settleChildProofStops(); await settleLandLifecycleDrives();
81
81
  meta.events.push({ kind: "land", id: node.id }); save();
82
82
  }
@@ -1,8 +1,14 @@
1
1
  import type { WorkflowRunRecord } from "@humwick/kernel/workflow.pilot";
2
2
  import { getTaskDetailAsync } from "../db";
3
3
  import { taskOwnerTarget } from "../task-owner";
4
+ import { delegatedTaskOwner, workflowCallerGone } from "./workflow-caller";
4
5
 
5
- /** Notices and decisions share the starter, then durable Task owner, then run coordinator. */
6
+ /** Notices and decisions share the starter, then the Task's live owning sub-coordinator (#3769), then its durable
7
+ * owner, then the run coordinator: the root hears of a sub-coordinator's Task only once that owner is gone. */
6
8
  export async function workflowCoordinatorTarget(run: WorkflowRunRecord): Promise<string> {
7
- return run.callerAgentId ?? taskOwnerTarget(await getTaskDetailAsync(run.taskId)) ?? run.coordinator;
9
+ if (run.callerAgentId) return run.callerAgentId;
10
+ const task = await getTaskDetailAsync(run.taskId);
11
+ const owner = task ? delegatedTaskOwner(task) : null;
12
+ if (owner && !workflowCallerGone(owner.id)) return owner.id;
13
+ return taskOwnerTarget(task) ?? run.coordinator;
8
14
  }
@@ -42,7 +42,7 @@ export async function performWorkflowDecision(run: WorkflowRunRecord, effect: Wo
42
42
  workflow: { runId: run.id, step: run.step, effectId: effect.id, waitKey: effect.waitKey!, ...(multi ? { items } : {}), ...(step.outputSchema ? { outputSchema: step.outputSchema } : {}) },
43
43
  };
44
44
  if (spec.answeredBy === "coordinator") {
45
- const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
45
+ const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
46
46
  subject: `Workflow ${run.id}: ${request.title}`,
47
47
  body: [request.title, request.context, `Choices (JSON data): ${JSON.stringify(items)}`,
48
48
  `Answer with relay_workflow_resume({runId: "${run.id}", decision: {action: "answer", waitKey: "${effect.waitKey}", answers: {${multi ? 'approved: ["item-id"]' : 'choice: "option-id"'}}}, note: "reason"}).`,
@@ -63,16 +63,16 @@ export function workflowDecisionSignal(effectId: string): Record<string, unknown
63
63
  return { outcome: "complete", decisionId: decision.id, outputs: workflowDecisionAnswer(decision, decision.answers!, decision.note) };
64
64
  }
65
65
 
66
- export function performWithdrawWorkflowDecision(effect: WorkflowEffect): Record<string, unknown> {
67
- withdrawWorkflowDecision(String(effect.payload.effectId));
66
+ export async function performWithdrawWorkflowDecision(effect: WorkflowEffect): Promise<Record<string, unknown>> {
67
+ (await withdrawWorkflowDecision(String(effect.payload.effectId)));
68
68
  return { withdrawn: true };
69
69
  }
70
70
 
71
71
  async function fileOperatorDecision(run: WorkflowRunRecord, request: WorkflowDecisionRequest, note?: string): Promise<Record<string, unknown>> {
72
72
  const task = await getTaskDetailAsync(run.taskId);
73
- const decision = fileWorkflowDecision({ actor: { id: run.coordinator, kind: "agent" }, callerAgentId: run.coordinator, scopes: ["*", "command:spawn"] }, {
73
+ const decision = (await fileWorkflowDecision({ actor: { id: run.coordinator, kind: "agent" }, callerAgentId: run.coordinator, scopes: ["*", "command:spawn"] }, {
74
74
  ...request,
75
- }, request.workflow, task?.projectId, note);
75
+ }, request.workflow, task?.projectId, note));
76
76
  return { decisionId: decision.id };
77
77
  }
78
78
 
@@ -31,7 +31,7 @@ async function resumeHosts(sup: Supervisor, orchestratorId?: string): Promise<vo
31
31
 
32
32
  export async function notifyDeferredWorkflowCleanup(run: WorkflowRunRecord, effect: WorkflowEffect): Promise<void> {
33
33
  const proof = run.landed?.sha ? await workflowLandVerification(run, run.landed.sha) : undefined;
34
- notifySystemMessage(await workflowCoordinatorTarget(run), {
34
+ await notifySystemMessage(await workflowCoordinatorTarget(run), {
35
35
  subject: `Workflow ${run.id}: cleanup deferred`,
36
36
  body: `Workflow ${run.id}: ${proof?.verified ? `landed as ${run.landed?.sha}. verified: true.` : proof?.line ?? "No land receipt."} Shutdowns on offline hosts are recorded durably and will run when those hosts re-register. The run can finish with its CI result.`,
37
37
  payload: { ...(proof ? { verified: proof.verified } : {}), kind: "workflow.cleanup-deferred", runId: run.id, effectId: effect.id, deferred: effect.result?.deferred },
@@ -19,12 +19,14 @@ import { ServiceAuthError } from "./errors";
19
19
 
20
20
  import { performWorkflowEffect } from "./workflow-effects";
21
21
  import { workflowCleanupDeferred } from "./workflow-deferred-cleanup";
22
+ import { PinnedHostCapacityError } from "../spawn-admission";
22
23
 
23
- import { invoke, live, runAgents, noteAgent, testOptions, type Supervisor } from "./workflow-supervisor-state";
24
+ import { effectDue, invoke, retryResult, live, runAgents, noteAgent, announceChange, supervisorNow, testOptions, type Supervisor } from "./workflow-supervisor-state";
24
25
  type Json = Record<string, unknown>;
25
26
  const EFFECT_BATCH = 50;
26
27
  const MAX_EFFECT_ATTEMPTS = 3;
27
28
  const REAP_RETRY_MS = 60 * 60_000;
29
+ const SLOT_WAIT_MS = 30 * 60_000;
28
30
  function isPermanentEffectError(error: unknown): boolean {
29
31
  return error instanceof WorkflowValueError || error instanceof ValidationError || error instanceof McpNotFoundError || error instanceof McpAuthError
30
32
  || error instanceof ServiceAuthError || error instanceof ReviewHoldConflictError;
@@ -39,21 +41,31 @@ async function settleEffect(sup: Supervisor, run: WorkflowRunRecord, effect: Wor
39
41
  run = { ...run, snapshot: root.snapshot };
40
42
  }
41
43
  try {
44
+ if (effect.kind === "dispatch" && !run.cancel && run.status === "running" && run.awaiting?.slot && run.awaiting.key === effect.waitKey && supervisorNow() >= run.awaiting.slot.deadline) {
45
+ throw new ValidationError(`Timed out waiting for a slot on ${run.awaiting.slot.host} after 30 minutes`);
46
+ }
42
47
  if (effect.kind === "dispatch" && run.rootRunId && !run.cancel) await invoke(sup, "reserveAgent", { runId: run.id, effectId: effect.id });
43
48
  if (effect.kind === "script" && !(await invoke<{ reserved: boolean }>(sup, "reserveScript", { runId: run.id, effectId: effect.id })).reserved) return false;
44
49
  const performed = effect.kind === "cancel-children" ? await cancelChildren(sup, run, effect) : effect.kind === "child" ? await createChild(sup, run, effect) : await performWorkflowEffect(run, effect, effect.kind === "reap" || effect.kind === "dispatch" ? await runAgents(sup, run.id) : [], (op, input) => invoke(sup, op, input));
45
50
  if (!performed && effect.kind !== "script") return false;
46
51
  result = performed ?? { pending: true };
47
52
  } catch (error) {
53
+ if (effect.kind === "dispatch" && error instanceof PinnedHostCapacityError) {
54
+ if (!run.awaiting?.slot) {
55
+ await invoke(sup, "waitForSlot", { id: effect.id, host: error.orchestratorId, now: supervisorNow(), timeoutMs: SLOT_WAIT_MS });
56
+ announceChange();
57
+ }
58
+ return false;
59
+ }
48
60
  const message = errMessage(error);
49
61
  if (workflowOwnsAgentCleanup(run) && ["cancel-children", "reap"].includes(effect.kind)) {
50
62
  if (run.status !== "done") await invoke(sup, "cleanup", { runId: run.id, complete: false, error: message });
51
- await invoke(sup, "settleEffect", { id: effect.id, status: "pending", error: message });
63
+ await invoke(sup, "settleEffect", { id: effect.id, status: "pending", error: message, result: retryResult(effect.result, effect.result ?? {}, supervisorNow()) });
52
64
  return false;
53
65
  }
54
66
  const failed = isPermanentEffectError(error) || effect.attempts + 1 >= MAX_EFFECT_ATTEMPTS;
55
67
  console.warn(`workflow effect ${effect.id} ${failed ? "failed" : "will retry"}: ${message}`);
56
- await invoke(sup, "settleEffect", { id: effect.id, status: failed ? "failed" : "pending", error: message });
68
+ await invoke(sup, "settleEffect", { id: effect.id, status: failed ? "failed" : "pending", error: message, now: supervisorNow() });
57
69
  if (failed && effect.kind === "dispatch") await releaseUnusedDispatch(sup, effect);
58
70
  if (failed && effect.kind === "script") await invoke(sup, "releaseReservation", { effectId: effect.id });
59
71
  const refusal = failed && effect.kind === "dispatch" ? sup.refusable.get(run.id) : undefined;
@@ -81,14 +93,15 @@ async function settleEffect(sup: Supervisor, run: WorkflowRunRecord, effect: Wor
81
93
  // The reap becomes a dormant deferred-cleanup record until its host returns (#3658).
82
94
  const deferred = Array.isArray(result.deferred) && result.deferred.length > 0;
83
95
  if (deferred || Array.isArray(result.retry) && result.retry.length && (workflowOwnsAgentCleanup(run) || Date.now() - effect.createdAt < REAP_RETRY_MS)) {
84
- const pending = await invoke<WorkflowEffect>(sup, "settleEffect", { id: effect.id, status: deferred && !(Array.isArray(result.retry) && result.retry.length) ? "deferred" : "pending", result, error: JSON.stringify(result.retry ?? result.deferred).slice(0, 2000) });
96
+ const status = deferred && !(Array.isArray(result.retry) && result.retry.length) ? "deferred" : "pending";
97
+ const pending = await invoke<WorkflowEffect>(sup, "settleEffect", { id: effect.id, status, result: status === "pending" ? retryResult(effect.result, result, supervisorNow()) : result, error: JSON.stringify(result.retry ?? result.deferred).slice(0, 2000) });
85
98
  if (!workflowCleanupDeferred(pending) && workflowOwnsAgentCleanup(run) && run.status !== "done") await invoke(sup, "cleanup", { runId: run.id, complete: false, error: JSON.stringify(result.retry).slice(0, 2000) });
86
99
  return workflowCleanupDeferred(pending);
87
100
  }
88
101
  if (effect.kind === "reap" && run.status === "cancelled" && !run.rootRunId) await invoke(sup, "cleanup", { runId: run.id, complete: true });
89
102
  if (effect.kind === "dispatch" && !result.spawnRequestId) await releaseUnusedDispatch(sup, effect);
90
103
  if (effect.kind === "script" && result.pending !== true) await invoke(sup, "releaseReservation", { effectId: effect.id });
91
- await invoke(sup, "settleEffect", { id: effect.id, status: "done", result });
104
+ await invoke(sup, "settleEffect", { id: effect.id, status: "done", result, now: supervisorNow() });
92
105
  return true;
93
106
  }
94
107
 
@@ -145,23 +158,33 @@ async function createChild(sup: Supervisor, run: WorkflowRunRecord, effect: Work
145
158
  return { childRunId: child.id };
146
159
  }
147
160
 
161
+ /** Perform up to a batch of due pending effects. A backing-off effect is skipped before any read and does not
162
+ * count toward the batch, so effects that never converge cannot starve newer ones (#3827). */
148
163
  export async function drainEffects(sup: Supervisor): Promise<number> {
149
- const { items } = await invoke<{ items: WorkflowEffect[] }>(sup, "pendingEffects", { limit: EFFECT_BATCH });
164
+ const now = supervisorNow();
150
165
  let settled = 0;
151
- for (const effect of items) {
152
- if (live() !== sup) break;
153
- try {
154
- if (effect.kind === "cancel-agent-tree") {
155
- if (await performSpawnTreeCleanup(sup, effect)) settled++;
156
- continue;
166
+ let attempted = 0;
167
+ let cursor: string | undefined;
168
+ do {
169
+ const page = await invoke<{ items: WorkflowEffect[]; cursor?: string }>(sup, "pendingEffects", { limit: EFFECT_BATCH, ...(cursor ? { cursor } : {}) });
170
+ for (const effect of page.items) {
171
+ if (live() !== sup) return settled;
172
+ if (!effectDue(effect, now)) continue;
173
+ if (attempted++ === EFFECT_BATCH) return settled;
174
+ try {
175
+ if (effect.kind === "cancel-agent-tree") {
176
+ if (await performSpawnTreeCleanup(sup, effect)) settled++;
177
+ continue;
178
+ }
179
+ const run = await invoke<WorkflowRunRecord>(sup, "get", { runId: effect.runId });
180
+ // #3757 — a paused run spawns, messages, arms and reaps nothing until its unpause; only its pause notices go out.
181
+ if (run.pause && effect.kind !== "pause-notice") continue;
182
+ if (await settleEffect(sup, run, effect)) settled += 1;
183
+ } catch (error) {
184
+ console.warn(`workflow effect ${effect.id} could not be settled: ${errMessage(error)}`);
157
185
  }
158
- const run = await invoke<WorkflowRunRecord>(sup, "get", { runId: effect.runId });
159
- // #3757 — a paused run spawns, messages, arms and reaps nothing until its unpause; only its pause notices go out.
160
- if (run.pause && effect.kind !== "pause-notice") continue;
161
- if (await settleEffect(sup, run, effect)) settled += 1;
162
- } catch (error) {
163
- console.warn(`workflow effect ${effect.id} could not be settled: ${errMessage(error)}`);
164
186
  }
165
- }
187
+ cursor = page.cursor;
188
+ } while (cursor);
166
189
  return settled;
167
190
  }
@@ -1,6 +1,5 @@
1
1
  import { planCiNotice } from "./plan-ci-notice";
2
2
  import { performWorkflowDecision, performWithdrawWorkflowDecision } from "./workflow-decisions";
3
- import { getCommandAsync } from "../commands-write";
4
3
  // #3045 — the src half of a workflow run: the kernel writes effect intents and waits; this module
5
4
  // performs each effect through the existing services and reads the durable facts that end a wait.
6
5
  // Every effect is keyed by its intent id, so a re-drive after a crash repeats nothing.
@@ -37,12 +36,15 @@ import { isStewardHandbackPendingError } from "../db/withdraw-land-error";
37
36
  import { ShutdownAuthError, ShutdownTargetError, shutdownAgent, UnlandedWorkRefusal } from "./shutdown-agent";
38
37
  import { OfflineControlHostError, resolveControlOrchestrator } from "./agent-control-host";
39
38
  import { workflowCoordinatorTarget } from "./workflow-coordinator";
39
+ import { supervisorNow } from "./workflow-supervisor-state";
40
+ import { isTerminalCommandStatus } from "../db/command-terminal-status";
41
+ import { isCoordinatorSeat } from "./workflow-caller";
40
42
  import { deliveredWorkspace, ensureWorkflowLanding, landedSignal, WORKFLOW_CTX } from "./workflow-land";
41
43
  import type { AuthContext } from "./auth-context";
42
44
  import type { Command, TaskDetail } from "../types";
43
45
 
44
46
  type Json = Record<string, unknown>;
45
- type WorkflowOp = (op: string, input: Json) => Promise<unknown>;
47
+ export type WorkflowOp = (op: string, input: Json) => Promise<unknown>;
46
48
 
47
49
  function short(sha: string | undefined): string {
48
50
  return sha ? sha.slice(0, 12) : "an unknown head";
@@ -169,16 +171,21 @@ async function createStepTask(run: WorkflowRunRecord, effect: WorkflowEffect): P
169
171
  return task.id;
170
172
  }
171
173
 
172
- /** The builder of record if it still runs: the agent a loop's findings go to (warm, never respawned). */
173
- export async function liveBuilder(taskId: number): Promise<{ id: string | undefined; live: boolean }> {
174
- const id = (await requireTask(taskId)).ownerAgentId ?? undefined;
174
+ /** The builder of record if it still runs: the agent a loop's findings go to (warm, never respawned). It is the run's
175
+ * newest registered agent on the input Task, else the Task's assigned agent unless that is a coordinator seat (#3769):
176
+ * a sub-coordinator assigned the Task owns its runs and never builds them. */
177
+ export async function liveBuilder(run: Pick<WorkflowRunRecord, "id" | "taskId">, call: WorkflowOp): Promise<{ id: string | undefined; live: boolean }> {
178
+ const assigned = (await requireTask(run.taskId)).ownerAgentId ?? undefined;
179
+ const { items } = await call("runAgents", { runId: run.id }) as { items: WorkflowAgent[] };
180
+ const id = items.filter((entry) => entry.taskId === run.taskId && entry.agentId).at(-1)?.agentId
181
+ ?? (assigned && !isCoordinatorSeat(getAgent(assigned)) ? assigned : undefined);
175
182
  const agent = id ? getAgent(id) : null;
176
183
  return { id, live: Boolean(agent && agent.status !== "offline") };
177
184
  }
178
185
 
179
- async function warmBuilder(taskId: number): Promise<string> {
180
- const builder = await liveBuilder(taskId);
181
- if (!builder.live) throw new ValidationError(`the builder of T${taskId} (${builder.id ?? "none bound"}) is gone, so the findings cannot reach it`);
186
+ async function warmBuilder(run: WorkflowRunRecord, call: WorkflowOp): Promise<string> {
187
+ const builder = await liveBuilder(run, call);
188
+ if (!builder.live) throw new ValidationError(`the builder of T${run.taskId} (${builder.id ?? "none bound"}) is gone, so the findings cannot reach it`);
182
189
  return builder.id!;
183
190
  }
184
191
 
@@ -252,14 +259,19 @@ function reapTargets(run: WorkflowRunRecord, effect: WorkflowEffect, agents: rea
252
259
  && (agentIds ? agentIds.has(agent.id) : workflowReapIncludesAgent(run, effect.payload, agent)));
253
260
  }
254
261
 
262
+ /** A stop the host found no target for is retried this rarely: a missing target does not reappear each pass. */
263
+ const TARGET_NOT_FOUND_RETRY_MS = 15 * 60_000;
264
+
255
265
  /** The run's latest stop command for an agent. A failed or timed-out one is terminal and never runs again,
256
- * so the next attempt gets its own id; a pump therefore retries the stop instead of rereading the failure. */
257
- export async function currentStop(baseId: string, retryFailed: (command: Command) => boolean = () => true): Promise<{ id: string; prior?: Command }> {
258
- for (let attempt = 1; ; attempt++) {
259
- const id = attempt === 1 ? baseId : `${baseId}:${attempt}`;
260
- const prior = await getCommandAsync(id);
261
- if (!prior || (prior.status !== "failed" && prior.status !== "timed_out") || !retryFailed(prior)) return { id, ...(prior ? { prior } : {}) };
262
- }
266
+ * so the next attempt gets its own id; a pump therefore retries the stop instead of rereading the failure.
267
+ * One read of the newest attempt, whatever the history (#3827). */
268
+ export async function currentStop(baseId: string, now = supervisorNow()): Promise<{ id: string; prior?: Command }> {
269
+ const latest = await getGateway().read("commands.latestAttempt", { baseId });
270
+ if (!latest) return { id: baseId };
271
+ const { attempt, command } = latest;
272
+ const retry = (command.status === "failed" || command.status === "timed_out")
273
+ && (command.result?.targetNotFound !== true || now - command.updatedAt >= TARGET_NOT_FOUND_RETRY_MS);
274
+ return retry ? { id: `${baseId}:${attempt + 1}` } : { id: command.id, prior: command };
263
275
  }
264
276
 
265
277
  /** Shut each target's live generation down through the shutdown service. The unlanded-work guard's
@@ -269,6 +281,7 @@ async function reapAgents(run: WorkflowRunRecord, effect: WorkflowEffect, target
269
281
  const kept: Array<{ spawnRequestId: string; reason: string }> = [];
270
282
  const retry: Array<{ spawnRequestId: string; reason: string }> = [];
271
283
  const deferred: Array<{ spawnRequestId: string; orchestratorId: string }> = [];
284
+ let inFlight = false;
272
285
  for (const agent of targets) {
273
286
  const current = agent.agentId ? getAgent(agent.agentId) : null;
274
287
  const agentId = current?.meta?.spawnRequestId === agent.spawnRequestId ? agent.agentId : undefined;
@@ -289,6 +302,7 @@ async function reapAgents(run: WorkflowRunRecord, effect: WorkflowEffect, target
289
302
  if (stopId && command.type === "agent.spawn" && command.status === "canceled") command = (await shutdownAgent(stop, WORKFLOW_CTX)).command;
290
303
  if (stopId && (command.status !== "succeeded" || command.result?.processTreeStopped !== true)) {
291
304
  retry.push({ spawnRequestId: agent.spawnRequestId, reason: `shutdown ${command.id}: ${command.status}` });
305
+ if (!isTerminalCommandStatus(command.status)) inFlight = true;
292
306
  continue;
293
307
  }
294
308
  reaped.push({ spawnRequestId: agent.spawnRequestId, ...(agent.agentId ? { agentId: agent.agentId } : {}) });
@@ -299,7 +313,7 @@ async function reapAgents(run: WorkflowRunRecord, effect: WorkflowEffect, target
299
313
  else retry.push(entry);
300
314
  }
301
315
  }
302
- return { reaped, ...(kept.length ? { kept } : {}), ...(retry.length ? { retry } : {}), ...(deferred.length ? { deferred } : {}) };
316
+ return { reaped, ...(kept.length ? { kept } : {}), ...(retry.length ? { retry } : {}), ...(deferred.length ? { deferred } : {}), ...(inFlight ? { inFlight } : {}) };
303
317
  }
304
318
 
305
319
  /** What a timed-out wait was waiting for, in words. */
@@ -360,7 +374,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
360
374
  if (run.cancel && ["dispatch", "findings", "clear-hold", "hold", "close"].includes(effect.kind)) return { stale: true };
361
375
  switch (effect.kind) {
362
376
  case "decision": return performWorkflowDecision(run, effect, call);
363
- case "withdraw-decision": return performWithdrawWorkflowDecision(effect);
377
+ case "withdraw-decision": return (await performWithdrawWorkflowDecision(effect));
364
378
  case "cancel-children": throw new ValidationError("tree cancellation is owned by the supervisor");
365
379
  case "child": throw new ValidationError("child effects are owned by the workflow supervisor");
366
380
  case "close": return closeWorkflowTask(run, effect);
@@ -368,7 +382,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
368
382
  case "started": {
369
383
  const quietFor = await planQuietingRunNotice(run.taskId, "started");
370
384
  if (quietFor) return { quiet: "plan-dispatched", planId: quietFor };
371
- const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
385
+ const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
372
386
  subject: `${runLabel(run)} started`,
373
387
  body: `${runLabel(run)} started at step ${run.definition.start}. You hear back only on a spawn failure, an escalation or the result; relay_workflow_get shows the run and its agents.`,
374
388
  idempotencyKey: key,
@@ -379,7 +393,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
379
393
  }
380
394
  case "landed": return { superseded: "the CI verdict owns the coordinator notice" };
381
395
  case "pause-notice": {
382
- const message = notifySystemMessage(await workflowCoordinatorTarget(run), { ...workflowPauseNotice(runLabel(run), run, effect), idempotencyKey: key, replyExpected: false });
396
+ const message = await notifySystemMessage(await workflowCoordinatorTarget(run), { ...workflowPauseNotice(runLabel(run), run, effect), idempotencyKey: key, replyExpected: false });
383
397
  return { messageId: message.id };
384
398
  }
385
399
  case "reap":
@@ -395,9 +409,9 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
395
409
  } else if (typeof effect.payload.taskId === "number") await holdTask(run, "routes its review");
396
410
  if (typeof effect.payload.taskId === "number") {
397
411
  const taskId = effect.payload.taskId;
398
- const builder = resume ? await liveBuilder(taskId) : undefined;
412
+ const builder = resume ? await liveBuilder({ id: run.id, taskId }, call) : undefined;
399
413
  if (builder?.live) {
400
- const message = notifySystemMessage(builder.id!, {
414
+ const message = await notifySystemMessage(builder.id!, {
401
415
  subject: `${runLabel(run)} resumed at ${String(effect.payload.step)}`,
402
416
  body: brief!,
403
417
  idempotencyKey: key,
@@ -413,9 +427,9 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
413
427
  return dispatchRole(run, effect, agents, await createStepTask(run, effect), role, brief, call);
414
428
  }
415
429
  case "findings": {
416
- const builder = await warmBuilder(run.taskId);
430
+ const builder = await warmBuilder(run, call);
417
431
  const head = str(effect.payload.head);
418
- const message = notifySystemMessage(builder, {
432
+ const message = await notifySystemMessage(builder, {
419
433
  subject: `${runLabel(run)}: ${String(effect.payload.step)} returned NO-GO on ${short(head)}`,
420
434
  body: [
421
435
  `${String(effect.payload.step)} (T${String(effect.payload.taskId)}) returned NO-GO on head ${head ?? "(unknown)"}. Round ${String(effect.payload.round)} starts now.`,
@@ -438,7 +452,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
438
452
  const head = str(effect.payload.head);
439
453
  const ensured = await ensureWorkflowLanding(run, effect);
440
454
  if (ensured.outcome === "REFUSED" && ensured.held === true) {
441
- const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
455
+ const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
442
456
  subject: `${runLabel(run)}: GO, awaiting your review hold release`,
443
457
  body: `${runLabel(run)}: review returned GO on ${head ?? "an unknown head"}. Your review hold stays set; nothing is submitted for landing. After your review, clear it with relay_task_set_review_hold (taskId: ${run.taskId}, held: false, expectedHeadSha: ${head ?? "the held head"}). The run waits for the land.\n${workflowNotice(run, [])}`,
444
458
  idempotencyKey: key,
@@ -457,7 +471,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
457
471
  if (effect.payload.reason === "ci-red" || effect.payload.reason === "ci-late") {
458
472
  const sha = str(effect.payload.landedSha) ?? run.landed?.sha ?? "";
459
473
  const proof = await workflowLandVerification(run, sha);
460
- const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
474
+ const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
461
475
  subject: `${runLabel(run)} ${proof.verified ? "landed" : "land verification FAILED"}, CI on main ${effect.payload.reason === "ci-red" ? "red" : "late"}`,
462
476
  body: workflowNotice(run, [proof.verified ? `T${run.taskId} landed as ${sha}. ${proof.line}` : proof.line, ...(effect.payload.reason === "ci-red" ? ciRedLead(runLabel(run), effect.payload) : [ciResultLine(effect.payload.ci as WorkflowCiVerdict)!])]),
463
477
  idempotencyKey: `workflow:landed:${run.id}:${sha}:${effect.waitKey ?? effect.id}`,
@@ -471,7 +485,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
471
485
  const lead = effect.payload.reason === "run-overdue" ? await workflowOverdueLead(run, effect.payload) : escalationLead(run, effect.payload);
472
486
  if ((effect.payload.reason === "step-timeout" || effect.payload.reason === "run-overdue")
473
487
  && isRecord(effect.payload.waitingFor) && effect.payload.waitingFor.kind === "landed") lead.push(await workflowLandNotice(run));
474
- const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
488
+ const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
475
489
  subject: `${runLabel(run)} ${run.status === "failed" ? "failed" : "escalated"}`,
476
490
  body: workflowNotice(run, [...(run.status === "failed" ? [`Script failed: ${String(effect.payload.reason)}`] : lead), `Outcome: ${effect.payload.alert ? "still waiting" : run.status === "failed" ? "failed" : "escalated"}. Landed: ${run.landed?.sha ?? "none"}. Reason: ${String(effect.payload.reason)}.`, ciResultLine(isRecord(effect.payload.ci) ? effect.payload.ci as WorkflowCiVerdict : undefined) ?? "CI on main: pending or not configured."]),
477
491
  idempotencyKey: key,
@@ -507,7 +521,7 @@ export async function performWorkflowEffect(run: WorkflowRunRecord, effect: Work
507
521
  const nothingToLand = effect.payload.nothingToLand === true || child?.output?.nothingToLand === true;
508
522
  const rounds = typeof effect.payload.rounds === "number" ? effect.payload.rounds : child?.output?.rounds ?? child?.round;
509
523
  const outcome = !sha ? "finished its script steps" : !proof?.verified ? `has an unverified land receipt for ${sha}` : nothingToLand ? `had nothing to land: its reviewed head is already on main as ${sha}` : `landed as ${sha}`;
510
- const message = notifySystemMessage(await workflowCoordinatorTarget(run), {
524
+ const message = await notifySystemMessage(await workflowCoordinatorTarget(run), {
511
525
  subject: `${runLabel(run)} ${proof?.verified === false ? "land verification FAILED" : "done"}${nothingToLand ? ", nothing to land" : ""}${ci?.status === "fail" ? ", CI on main red" : ""}`,
512
526
  body: workflowNotice(run, [`${runLabel(run)} is done: T${run.taskId} ${outcome}${rounds === undefined ? "" : ` after ${rounds} round(s)`}.${proof ? ` ${proof.line}` : ""}`, ciLine ?? (child || !run.rootRunId ? "CI on main: not configured." : "CI on main: unknown.")], children.items, !!children.cursor),
513
527
  idempotencyKey: key,
@@ -1,7 +1,7 @@
1
1
  // #3121 — per-step pins on a workflow run: the wire parser every surface shares, and the start check that
2
2
  // resolves each step's launch (pin, role file and dispatch policy) through the spawn's own seam and refuses a
3
3
  // launch that cannot run before anything spawns. Precedence (call pin > YAML step) is the kernel's workflowStepPin.
4
- import { cleanString, isRecord, isSpawnProvider, SPAWN_PROVIDERS, stringValue, type SpawnProvider, type TaskDispatch } from "@humwick/sdk";
4
+ import { cleanString, isRecord, isSpawnProvider, SPAWN_PROVIDERS, stringValue, type SpawnProvider, type TaskDispatch, type WorkflowRunPins } from "@humwick/sdk";
5
5
  import { findProviderCatalogModel, type ProviderCatalogEntry } from "@humwick/sdk/provider-catalog";
6
6
  import {
7
7
  stampWorkflowPins, WORKFLOW_PIN_FIELDS, WorkflowDefinitionError, workflowStepPin,
@@ -15,11 +15,12 @@ import { sameHostIdentity } from "../utils";
15
15
  import { roleSpawnableProviders } from "./spawn-project-role";
16
16
  import { resolveSpawnLaunch, roleLaunchDefaults } from "./spawn-launch";
17
17
  import { ServiceAuthError } from "./errors";
18
+ import { PinnedHostCapacityError } from "../spawn-admission";
18
19
  import type { ProjectRole } from "../project-roles";
19
20
  import type { AgentCard } from "../types";
20
21
  import type { AuthContext } from "./auth-context";
21
22
 
22
- export type WorkflowPins = Record<string, WorkflowPin>;
23
+ export type WorkflowPins = WorkflowRunPins;
23
24
 
24
25
  export const PIN_SOURCE_LABEL = { start: "start pin", resume: "resume pin", team: "team default", global: "global default", workflow: "workflow file" } as const;
25
26
 
@@ -118,12 +119,14 @@ export async function assertStepLaunches(check: StepLaunchCheck, ctx: AuthContex
118
119
  const from = declared.crossProvider;
119
120
  const avoid = from && !pin?.provider ? check.avoided?.(from) ?? launched.get(from) : undefined;
120
121
  const defaults = roleLaunchDefaults(role);
121
- const launch = await resolveSpawnLaunch({
122
+ const input = {
122
123
  profile: defaults.profile, spawnCap: { callerAgentId: ctx.callerAgentId },
123
124
  provider: (pin?.provider as SpawnProvider | undefined) ?? defaults.provider, dispatchPin: { model: pin?.model, effort: pin?.effort },
124
125
  dispatchShape: stepTaskDispatch(check.inputTask, declared) ?? defaults.dispatchShape, orchestratorId: pin?.orchestratorId, ...(avoid ? { avoidProviders: [avoid] } : {}), projectRole: role,
125
126
  pinSources: pin?.from,
126
- }).catch((error: Error) => {
127
+ };
128
+ const launch = await resolveSpawnLaunch(input).catch((error: Error) => {
129
+ if (error instanceof PinnedHostCapacityError) return resolveSpawnLaunch({ ...input, spawnCap: { ...input.spawnCap, attach: true } });
127
130
  throw new ValidationError(`${where}: ${error.message}`);
128
131
  });
129
132
  const runnable = roleSpawnableProviders(pin?.orchestratorId, role, launch.catalog);
@@ -48,6 +48,7 @@ async function alertStalled(sup: Supervisor, run: WorkflowRunRecord, wait: Await
48
48
 
49
49
  /** A wait past its deadline alerts the coordinator once and keeps waiting, like a stopped land (#3045 H1). */
50
50
  async function alertTimeout(sup: Supervisor, run: WorkflowRunRecord, wait: Awaiting, taskId: number | undefined): Promise<boolean> {
51
+ if (wait.slot) return false;
51
52
  const timeoutMs = wait.deadline !== undefined ? wait.deadline - wait.since : waitTimeoutMs(run.definition, run.step);
52
53
  const now = workflowRunClock(run, supervisorNow());
53
54
  if (wait.timedOutAt !== undefined || now < wait.since + timeoutMs) return false;
@@ -4,16 +4,16 @@ import type { WorkflowAgent, WorkflowRunRecord } from "@humwick/kernel/workflow.
4
4
  import { getAgent, getTaskDetailAsync, ValidationError } from "../db";
5
5
  import type { ProjectRole } from "../project-roles";
6
6
  import { resolveSpawnProjectRole } from "./spawn-project-role";
7
- import { liveBuilder, stepAgentProvider } from "./workflow-effects";
7
+ import { liveBuilder, stepAgentProvider, type WorkflowOp } from "./workflow-effects";
8
8
  import { assertStepLaunches, unmetPinFields } from "./workflow-pins";
9
9
  import type { AuthContext } from "./auth-context";
10
10
 
11
11
  /** A resume's new pins: a live warm builder must already meet its step's pin, since the run hands it findings
12
12
  * instead of spawning and replacing it would take its workspace; and each named step's launch must resolve and run. */
13
- export async function assertResumePins(run: WorkflowRunRecord, pins: Record<string, WorkflowCallPin>, task: NonNullable<Awaited<ReturnType<typeof getTaskDetailAsync>>>, ctx: AuthContext, agents: readonly WorkflowAgent[]): Promise<void> {
13
+ export async function assertResumePins(run: WorkflowRunRecord, pins: Record<string, WorkflowCallPin>, task: NonNullable<Awaited<ReturnType<typeof getTaskDetailAsync>>>, ctx: AuthContext, agents: readonly WorkflowAgent[], call: WorkflowOp): Promise<void> {
14
14
  const merged = { ...run.pins, ...pins };
15
15
  const steps = Object.keys(run.definition.steps).filter((step) => pins[step]);
16
- const builder = await liveBuilder(run.taskId);
16
+ const builder = await liveBuilder(run, call);
17
17
  const agent = builder.live ? getAgent(builder.id!) : null;
18
18
  for (const step of steps.filter((name) => run.definition.steps[name]!.task === "input")) {
19
19
  const pin = workflowStepPin(run.definition, merged, step, run.routingDefaults);
@@ -0,0 +1,18 @@
1
+ import type { StepRoutingDefaults, TaskDetail } from "@humwick/sdk";
2
+ import type { WorkflowRoutingDefaults } from "@humwick/kernel/workflow-definition";
3
+ import { getAgent, getTeam } from "../db";
4
+ import { taskOwnerRoleTeamId } from "../db/task-owner";
5
+
6
+ /** #3829 — defaults follow the Task's durable owner, then its nearest configured ancestor. */
7
+ export function resolveWorkflowRoutingDefaults(defaults: StepRoutingDefaults | undefined, task: Pick<TaskDetail, "owner" | "teamId" | "createdBy">, callerAgentId?: string): WorkflowRoutingDefaults | undefined {
8
+ if (!defaults) return undefined;
9
+ const coordinatorId = callerAgentId ?? task.createdBy;
10
+ const teamId = taskOwnerRoleTeamId(task.owner) ?? task.teamId ?? (coordinatorId ? getAgent(coordinatorId)?.teamId : undefined);
11
+ const visited = new Set<string>();
12
+ for (let current = teamId; current && !visited.has(current); current = getTeam(current)?.parentTeamId) {
13
+ visited.add(current);
14
+ const team = defaults.teams?.[current];
15
+ if (team) return { global: defaults.global, team, teamId: current };
16
+ }
17
+ return { global: defaults.global, ...(teamId ? { teamId } : {}) };
18
+ }
@@ -6,9 +6,9 @@ import { workflowText } from "./workflow-notice";
6
6
 
7
7
  type Awaiting = NonNullable<WorkflowRunRecord["awaiting"]>;
8
8
 
9
- function awaitingView({ kind, taskId, sha, deadline, timedOutAt }: Awaiting): NonNullable<WorkflowRunView["awaiting"]> {
9
+ function awaitingView({ kind, taskId, sha, deadline, timedOutAt, slot }: Awaiting): NonNullable<WorkflowRunView["awaiting"]> {
10
10
  return {
11
- kind, ...(taskId !== undefined ? { taskId } : {}), ...(sha ? { sha } : {}), ...(deadline !== undefined ? { deadline } : {}), ...(timedOutAt !== undefined ? { timedOutAt } : {}),
11
+ kind, ...(taskId !== undefined ? { taskId } : {}), ...(sha ? { sha } : {}), ...(deadline !== undefined ? { deadline } : {}), ...(timedOutAt !== undefined ? { timedOutAt } : {}), ...(slot ? { slot } : {}),
12
12
  };
13
13
  }
14
14
 
@@ -23,7 +23,7 @@ export function runView(run: WorkflowRunRecord): WorkflowRunView {
23
23
  /** The declared steps in order; `land` follows the last. */
24
24
  steps: Object.entries(run.definition.steps).map(([name, step]) => {
25
25
  const pin = workflowStepPin(run.definition, run.pins, name, run.routingDefaults);
26
- return { name, role: step.role, ...(step.gate ? { gate: step.gate } : {}), ...(step.crossProvider ? { crossProvider: step.crossProvider } : {}), ...(pin ? { pin } : {}) };
26
+ return { name, role: step.role, started: run.entered.includes(name), ...(step.gate ? { gate: step.gate } : {}), ...(step.crossProvider ? { crossProvider: step.crossProvider } : {}), ...(pin ? { pin } : {}) };
27
27
  }),
28
28
  ...(run.definition.ciMain ? { ciMain: { ...(run.definition.ciMain.workflow ? { workflow: run.definition.ciMain.workflow } : {}) } } : {}),
29
29
  ref: run.ref,
@@ -43,6 +43,7 @@ export function runView(run: WorkflowRunRecord): WorkflowRunView {
43
43
  ...(run.pause ? { paused: run.pause } : {}),
44
44
  ...(run.reason ? { reason: run.reason } : {}),
45
45
  ...(run.resumes?.length ? { resumes: run.resumes } : {}),
46
+ ...(run.pinChanges?.length ? { pinChanges: run.pinChanges } : {}),
46
47
  ...(run.output ? { output: run.output } : {}),
47
48
  createdAt: run.createdAt,
48
49
  updatedAt: run.updatedAt,
@@ -90,7 +90,7 @@ export function installWorkflowHost(options: { confirmStops?: boolean } = {}): (
90
90
  export async function initWorkflowDb(): Promise<void> {
91
91
  await stopWorkflowServingForTests();
92
92
  await settleLandLifecycleDrives();
93
- flushTaskStatusPulses();
93
+ await flushTaskStatusPulses();
94
94
  initDb(":memory:");
95
95
  }
96
96