@humwick/server 0.130.2 → 0.130.3

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 (570) 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 +73 -4
  8. package/package.json +8 -7
  9. package/public/assets/{MessageBubble-BWLqMGKG.js → MessageBubble-Bq9kr21N.js} +2 -2
  10. package/public/assets/{PlanGraphPanel-3TlIQpjk.js → PlanGraphPanel-Dqiroy06.js} +2 -2
  11. package/public/assets/{TraceView-Wimm0Czv.js → TraceView-DYU962-H.js} +2 -2
  12. package/public/assets/{VaultDecisionCard-Bau_vNax.js → VaultDecisionCard-DhPfzioE.js} +2 -2
  13. package/public/assets/{activity-CrSurn-r.js → activity-DzCcQTee.js} +2 -2
  14. package/public/assets/{agent-lineage-CknAJKcb.js → agent-lineage-SgDt9i3T.js} +2 -2
  15. package/public/assets/agent-liveness-Bq-s73jW.js +2 -0
  16. package/public/assets/{agent-name-DKbiIhBo.js → agent-name-CcY3Aijy.js} +2 -2
  17. package/public/assets/{agent-profiles-DwF6EQ4R.js → agent-profiles-cxtRBn6O.js} +2 -2
  18. package/public/assets/{agent-project-badge-Diz4Sjjb.js → agent-project-badge-DGYbIhaI.js} +2 -2
  19. package/public/assets/{agent-tools-CRhpRQza.js → agent-tools-DHbftV0N.js} +2 -2
  20. package/public/assets/{agent-type-icon-LrUEU3lB.js → agent-type-icon-CY113sp-.js} +2 -2
  21. package/public/assets/{agents-B-v66C-4.js → agents-BPzY7IlQ.js} +2 -2
  22. package/public/assets/{analytics-BqCb-LKi.js → analytics-DENFPv_a.js} +2 -2
  23. package/public/assets/{artifact-viewer-kf4pGuAR.js → artifact-viewer-KRvSc7sl.js} +2 -2
  24. package/public/assets/{artifacts-DJevXcvg.js → artifacts-C9gSmL3s.js} +2 -2
  25. package/public/assets/{artifacts-D32zyw4Y.js → artifacts-DT-HkI5O.js} +2 -2
  26. package/public/assets/{automation-BpRajx_5.js → automation-BsmjQhKD.js} +2 -2
  27. package/public/assets/{backlog-D_UjTdzX.js → backlog-8NYH_62t.js} +2 -2
  28. package/public/assets/{busy-health-N3VEh4ao.js → busy-health-D3hwvYb4.js} +2 -2
  29. package/public/assets/{channels-9jopungq.js → channels-DCJ5WL7O.js} +2 -2
  30. package/public/assets/{chat-CSXrTB3C.js → chat-DAIl357U.js} +3 -3
  31. package/public/assets/{code-preview-p1NnmqK0.js → code-preview-CzrZfr9U.js} +2 -2
  32. package/public/assets/{connectors-CZdqlDNL.js → connectors-xTxtlyou.js} +2 -2
  33. package/public/assets/{cosmos-BgdsE4dG.js → cosmos-BpebFLK2.js} +2 -2
  34. package/public/assets/{crate-agent-30aXTKva.js → crate-agent-BHvI5wXC.js} +2 -2
  35. package/public/assets/{dependency-graph-B_jvw2GE.js → dependency-graph-DGT0o3wJ.js} +2 -2
  36. package/public/assets/{deploys-DoN6bAb5.js → deploys-hqWOHSHE.js} +2 -2
  37. package/public/assets/{display-BrKWFGJz.js → display-CDeH4I8T.js} +2 -2
  38. package/public/assets/{files-D6XEu8Ng.js → files-BTeGVmTg.js} +2 -2
  39. package/public/assets/{fleet-C7jQWbw2.js → fleet-DgXtNgCa.js} +2 -2
  40. package/public/assets/{fleet-metrics-BiDs3MLx.js → fleet-metrics-BHEt5yc_.js} +2 -2
  41. package/public/assets/{formatted-body-CQBwc1fb.js → formatted-body-MLtodyPp.js} +3 -3
  42. package/public/assets/{formatted-body-impl-DyG0WsQS.js → formatted-body-impl-CAe664Z8.js} +2 -2
  43. package/public/assets/{host-providers-dialog-DKaW31dN.js → host-providers-dialog-mapF4Fj9.js} +2 -2
  44. package/public/assets/{index-CRHep3M9.js → index-CqqgOI5R.js} +5 -5
  45. package/public/assets/{insights-CzrtkpOE.js → insights-DNyf7IzF.js} +2 -2
  46. package/public/assets/{integrations-aVsUmigt.js → integrations-DlYOHq7T.js} +2 -2
  47. package/public/assets/{issue-ref-chip-BnSlM_95.js → issue-ref-chip-CeWBNWn7.js} +2 -2
  48. package/public/assets/{log-viewer-qe7Ee47Z.js → log-viewer-Cib8nqdE.js} +2 -2
  49. package/public/assets/{maintenance-NfjqFZAk.js → maintenance-DSn1cPm7.js} +2 -2
  50. package/public/assets/{managed-agents-BiH0HgaE.js → managed-agents-C3CkM81m.js} +2 -2
  51. package/public/assets/{markdown-preview-BixaT3Xr.js → markdown-preview-W4VQQPAv.js} +3 -3
  52. package/public/assets/{markdown-preview-impl-DNGlWIS3.js → markdown-preview-impl-CMsTdhjq.js} +2 -2
  53. package/public/assets/{memory-DNsCl6UX.js → memory-BV17qtO4.js} +2 -2
  54. package/public/assets/{messages-DopU1L1X.js → messages-Bm_1AqSA.js} +2 -2
  55. package/public/assets/{orchestrators-Cq7ckLQT.js → orchestrators-Dg-vfP09.js} +2 -2
  56. package/public/assets/{overview-C1DCTzzV.js → overview-DcYCrvlr.js} +2 -2
  57. package/public/assets/{pairs-DE_m6rrE.js → pairs-8o9nuG3V.js} +2 -2
  58. package/public/assets/{profile-icon-B3rfh77F.js → profile-icon-BsRyBMea.js} +2 -2
  59. package/public/assets/{project-profile-instructions-DQWZE0kA.js → project-profile-instructions-DETKFn_E.js} +2 -2
  60. package/public/assets/{projects-Br2Bd2h1.js → projects-M_m6AHws.js} +2 -2
  61. package/public/assets/{prompt-settings-BKvFsvGD.js → prompt-settings-C_mNCgeL.js} +2 -2
  62. package/public/assets/provider-display-BEeLXC_D.js +2 -0
  63. package/public/assets/{registry-Cw38_6gW.js → registry-DDHHHE5N.js} +2 -2
  64. package/public/assets/run-card-Dt4I4jaP.js +2 -0
  65. package/public/assets/{secrets-DxI-vzSq.js → secrets-BkVwxI5O.js} +2 -2
  66. package/public/assets/{service-access-settings-D9s_VJbd.js → service-access-settings-Crq-6a-B.js} +2 -2
  67. package/public/assets/{settings-CKZTVGIx.js → settings-CYcavMFu.js} +2 -2
  68. package/public/assets/shipping-H6gzEeZB.js +2 -0
  69. package/public/assets/{stats-DyUBKyWI.js → stats-BJVeWkpV.js} +2 -2
  70. package/public/assets/{status-dot-DYqlEUhJ.js → status-dot-BcHyw1Vw.js} +2 -2
  71. package/public/assets/store-DKPDIZBe.js +9 -0
  72. package/public/assets/{tasks-Bh7Chr4-.js → tasks-qJjlQPpx.js} +2 -2
  73. package/public/assets/{teams-bowI7qVY.js → teams-Cd5Sk1uX.js} +2 -2
  74. package/public/assets/{terminal-viewer-BgVLHdsU.js → terminal-viewer-CpY5_m7W.js} +3 -3
  75. package/public/assets/{terminal-viewer-impl-Dz22UPy6.js → terminal-viewer-impl-DI_2llFR.js} +2 -2
  76. package/public/assets/{use-agent-terminal-DSQQ-iY7.js → use-agent-terminal-0yJA_buM.js} +2 -2
  77. package/public/assets/{use-decisions-samMQhQf.js → use-decisions-uJXHf34W.js} +2 -2
  78. package/public/assets/{use-project-list-59LheVP7.js → use-project-list-CpWl6eHQ.js} +2 -2
  79. package/public/assets/{use-selectors-BZa25xEB.js → use-selectors-BdxBW8mL.js} +2 -2
  80. package/public/assets/{user-avatar-DHxlvUvV.js → user-avatar-3IndfpOc.js} +2 -2
  81. package/public/assets/{versioned-prompt-editor-BmED5DnJ.js → versioned-prompt-editor-CIVg7XB1.js} +2 -2
  82. package/public/assets/{work-queue-CSvpuNLz.js → work-queue-B9rnLwHZ.js} +2 -2
  83. package/public/assets/{workflow-marker-DAZcj5qt.js → workflow-marker-X8wxyhzC.js} +2 -2
  84. package/public/assets/{workflows-DdmxRSyg.js → workflows-o5YyMFW7.js} +2 -2
  85. package/public/assets/{workspaces-BxeKfVuF.js → workspaces-CzEGoAng.js} +2 -2
  86. package/public/cosmos.html +4 -4
  87. package/public/index.html +28 -28
  88. package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
  89. package/runner/src/adapters/claude-headless.ts +1 -1
  90. package/runner/src/adapters/claude-launch-exit.ts +26 -0
  91. package/runner/src/adapters/claude-session-probe.ts +1 -1
  92. package/runner/src/adapters/claude-tmux.ts +38 -10
  93. package/runner/src/adapters/claude.ts +28 -75
  94. package/runner/src/adapters/provider-quota-harvest.ts +20 -71
  95. package/runner/src/agent-runner/lifecycle-milestones.ts +2 -2
  96. package/runner/src/agent-runner/liveness-exchange.ts +104 -0
  97. package/runner/src/claude-system-prompt.ts +49 -0
  98. package/runner/src/launch-assembly.ts +14 -10
  99. package/runner/src/profile-home.ts +1 -1
  100. package/runner/src/project-instructions.ts +2 -2
  101. package/runner/src/provider-command.ts +18 -0
  102. package/runner/src/provider-setup.ts +2 -1
  103. package/runner/src/runner-core.ts +14 -15
  104. package/runner/src/runner-helpers.ts +0 -8
  105. package/runner/src/session-scratch.ts +6 -2
  106. package/runner/test-support/provisioning-context.ts +4 -2
  107. package/runner/test-support/runner-harness.ts +2 -1
  108. package/scripts/steward-policy-cleanup.ts +2 -2
  109. package/src/agent-authz.ts +2 -2
  110. package/src/agent-drive-arbiter.ts +3 -8
  111. package/src/agent-exit-cleanup-async.ts +2 -2
  112. package/src/agent-exit-cleanup.ts +23 -19
  113. package/src/agent-lifecycle-events.ts +112 -85
  114. package/src/agent-ref.ts +3 -2
  115. package/src/agent-status-events.ts +43 -14
  116. package/src/auth/password.ts +4 -3
  117. package/src/auth/session-refresh.ts +1 -1
  118. package/src/auth/session.test-fixture.ts +2 -2
  119. package/src/auth/session.ts +7 -7
  120. package/src/automations/crud.ts +8 -81
  121. package/src/automations/dispatch.ts +33 -58
  122. package/src/automations/index.ts +1 -1
  123. package/src/automations/persistence.ts +65 -0
  124. package/src/automations/precheck.ts +18 -16
  125. package/src/automations/read-async.ts +11 -0
  126. package/src/automations/reconcile.ts +30 -39
  127. package/src/automations/storage.ts +33 -10
  128. package/src/automations/write-async.ts +33 -0
  129. package/src/branch-land-notice.ts +3 -3
  130. package/src/branch-landed.ts +25 -23
  131. package/src/bus.ts +21 -19
  132. package/src/cd-deploy-notify.ts +2 -2
  133. package/src/ci/notifications.ts +4 -4
  134. package/src/ci/root-missing.ts +3 -3
  135. package/src/ci/transition-notify.ts +5 -5
  136. package/src/claude-app/oauth-http.ts +3 -3
  137. package/src/cli/workflow.ts +12 -3
  138. package/src/commands-db.ts +4 -4
  139. package/src/commands-write.ts +8 -0
  140. package/src/compaction-watch.ts +10 -8
  141. package/src/config-store-write.ts +2 -2
  142. package/src/config.ts +4 -11
  143. package/src/connectors.ts +10 -8
  144. package/src/context-advisory.ts +22 -21
  145. package/src/db/agent-action-write.ts +36 -0
  146. package/src/db/agent-control-async.ts +9 -0
  147. package/src/db/agent-control-write.ts +49 -0
  148. package/src/db/agent-effort-observe.ts +22 -0
  149. package/src/db/agent-meta.ts +14 -3
  150. package/src/db/agents-async.ts +8 -5
  151. package/src/db/agents.ts +15 -12
  152. package/src/db/auth-identities.ts +2 -2
  153. package/src/db/automation-read-ops.ts +11 -0
  154. package/src/db/automation-write-ops.ts +46 -0
  155. package/src/db/blackboard.ts +2 -2
  156. package/src/db/browser-session-write.ts +32 -0
  157. package/src/db/bus-outbox-write-ops.ts +3 -1
  158. package/src/db/checkout-refresh-schema.ts +47 -0
  159. package/src/db/checkout-refresh.ts +5 -5
  160. package/src/db/command-agent-profile-backfill.ts +39 -0
  161. package/src/db/command-agent-profiles.ts +5 -1
  162. package/src/db/command-retention.ts +126 -0
  163. package/src/db/commands-planner-stats.test-fixture.ts +8 -0
  164. package/src/db/commands-read-ops.ts +16 -0
  165. package/src/db/connection.ts +1 -182
  166. package/src/db/context-carry-write.ts +74 -0
  167. package/src/db/continuations.ts +4 -4
  168. package/src/db/db-error-registry.ts +12 -0
  169. package/src/db/gateway.ts +26 -133
  170. package/src/db/history-retention-ops.ts +13 -0
  171. package/src/db/inbox-async.ts +2 -2
  172. package/src/db/inbox-read-ops.ts +1 -1
  173. package/src/db/inbox.ts +9 -3
  174. package/src/db/issue-associations.ts +2 -2
  175. package/src/db/issue-refs-async.ts +4 -1
  176. package/src/db/issue-refs-read-ops.ts +6 -0
  177. package/src/db/issue-refs.ts +8 -8
  178. package/src/db/kernel-event-log-schema.ts +2 -0
  179. package/src/db/land-boundaries.ts +51 -5
  180. package/src/db/land-gate-attestations.ts +2 -2
  181. package/src/db/landing-receipts.ts +2 -2
  182. package/src/db/lifecycle-context-write.ts +5 -0
  183. package/src/db/lifecycle-tail-async.ts +76 -0
  184. package/src/db/lifecycle-tail-write-ops.ts +62 -0
  185. package/src/db/lifecycle-write-window.ts +2 -1
  186. package/src/db/lineage-pair-dedupe-sql.ts +3 -3
  187. package/src/db/main-connection-mode.ts +11 -0
  188. package/src/db/main-connection-writes.ts +20 -2
  189. package/src/db/maintenance-job-runs-async.ts +2 -2
  190. package/src/db/maintenance-job-write-ops.ts +1 -1
  191. package/src/db/maintenance-pauses.ts +2 -2
  192. package/src/db/message-send.ts +4 -0
  193. package/src/db/messages.ts +31 -25
  194. package/src/db/migrations.ts +18 -1
  195. package/src/db/notification-write-ops.ts +19 -0
  196. package/src/db/operator-answer-write.ts +19 -0
  197. package/src/db/orchestrators.ts +10 -4
  198. package/src/db/pairs-async.ts +22 -1
  199. package/src/db/pairs-write-ops.ts +8 -3
  200. package/src/db/pairs.ts +41 -48
  201. package/src/db/plan-dispatch-schema.ts +5 -1
  202. package/src/db/plan-ready-notices.ts +43 -0
  203. package/src/db/plan-tools-include-migration.ts +36 -0
  204. package/src/db/plans-write-ops.ts +2 -0
  205. package/src/db/presence-buffer.ts +5 -0
  206. package/src/db/presence-loop.ts +18 -23
  207. package/src/db/presence-write.ts +0 -6
  208. package/src/db/project-entries-async.ts +14 -0
  209. package/src/db/project-entries-read-ops.ts +5 -1
  210. package/src/db/project-entries-write-ops.ts +8 -0
  211. package/src/db/project-entries.ts +15 -12
  212. package/src/db/project-journal.ts +2 -2
  213. package/src/db/project-trust-approvals.ts +2 -2
  214. package/src/db/prompt-templates.ts +4 -4
  215. package/src/db/read-op-marshaling-fixtures.ts +18 -0
  216. package/src/db/read-ops.ts +4 -0
  217. package/src/db/retention.ts +13 -2
  218. package/src/db/review-publish-write.ts +8 -0
  219. package/src/db/runtime-admin-async.ts +19 -0
  220. package/src/db/runtime-admin-write-ops.ts +36 -0
  221. package/src/db/runtime-tail-async.ts +288 -0
  222. package/src/db/runtime-tail-write-ops.ts +97 -0
  223. package/src/db/schema.ts +4 -1
  224. package/src/db/spawn-budget.ts +5 -0
  225. package/src/db/spawn-delegation.ts +1 -11
  226. package/src/db/sql-audit-indexes.ts +3 -0
  227. package/src/db/stats-async.ts +5 -3
  228. package/src/db/task-entity.ts +5 -4
  229. package/src/db/team-claim-policy.ts +60 -0
  230. package/src/db/teams-async.ts +4 -0
  231. package/src/db/teams-write-ops.ts +4 -0
  232. package/src/db/terminal-events.ts +15 -0
  233. package/src/db/timer-cutover-bodies.ts +292 -0
  234. package/src/db/timer-cutover-core.ts +37 -278
  235. package/src/db/timer-cutover-state.ts +40 -0
  236. package/src/db/timer-cutover-write-ops.ts +1 -40
  237. package/src/db/token-db-write-ops.ts +2 -0
  238. package/src/db/token-standing-async.ts +12 -0
  239. package/src/db/token-standing.ts +6 -9
  240. package/src/db/token-usage-retention.ts +33 -0
  241. package/src/db/token-usage-write-ops.ts +1 -1
  242. package/src/db/token-usage.ts +4 -1
  243. package/src/db/user-invites-async.ts +13 -0
  244. package/src/db/user-invites.ts +3 -3
  245. package/src/db/users.ts +14 -6
  246. package/src/db/workspaces.ts +8 -5
  247. package/src/db/write-ops.ts +31 -12
  248. package/src/db/writer-fault-seam.ts +2 -9
  249. package/src/db/writer-fault-test-ops.ts +18 -0
  250. package/src/db/writer-gateway.ts +42 -46
  251. package/src/dev.ts +8 -3
  252. package/src/event-loop-lag.ts +4 -2
  253. package/src/event-loop-yield.ts +2 -2
  254. package/src/events.ts +10 -8
  255. package/src/host-provider-store.ts +5 -4
  256. package/src/hub-metrics-report.ts +5 -1
  257. package/src/hub-metrics.ts +38 -18
  258. package/src/injection-sweep-pacing.ts +6 -8
  259. package/src/land-gate-notifications.ts +11 -10
  260. package/src/land-gate-queue-progress.ts +3 -3
  261. package/src/land-settle-dispatch.ts +52 -92
  262. package/src/lifecycle-context-pressure.ts +11 -10
  263. package/src/lifecycle-manager.ts +104 -98
  264. package/src/maintenance/breaker.ts +18 -16
  265. package/src/maintenance/checkout-refresh.ts +18 -1
  266. package/src/maintenance/critical-job-watchdog.ts +3 -3
  267. package/src/maintenance/events.ts +3 -3
  268. package/src/maintenance/history-retention-job.ts +30 -0
  269. package/src/maintenance/jobs.ts +29 -67
  270. package/src/maintenance/long-running-work.ts +3 -1
  271. package/src/maintenance/offline-agent-prune-job.ts +28 -0
  272. package/src/maintenance/orphaned-sessions.ts +5 -4
  273. package/src/maintenance/pauses.ts +4 -10
  274. package/src/maintenance/plan-ready-notice-job.ts +71 -0
  275. package/src/maintenance/scheduler.ts +18 -13
  276. package/src/maintenance/stalled-delivery-pager.ts +4 -4
  277. package/src/maintenance/teams.ts +19 -30
  278. package/src/maintenance/workspaces.ts +17 -27
  279. package/src/managed-policy-escalation.ts +7 -7
  280. package/src/managed-policy.ts +3 -3
  281. package/src/managed-provider-gate.ts +5 -4
  282. package/src/managed-spawn-reservation.ts +5 -4
  283. package/src/mcp/index.ts +8 -8
  284. package/src/mcp/output-descriptions.ts +1 -1
  285. package/src/mcp/presentation.ts +2 -1
  286. package/src/mcp/runtime-output.ts +6 -1
  287. package/src/mcp/self-care-visibility.ts +2 -2
  288. package/src/mcp/spawn-targets-matrix.ts +10 -1
  289. package/src/mcp/tool-groups.ts +3 -2
  290. package/src/mcp/tool-roles.ts +81 -0
  291. package/src/mcp/tools-workspace.ts +1 -1
  292. package/src/mcp-blackboard-tools.ts +6 -5
  293. package/src/mcp-decision-tools.ts +7 -7
  294. package/src/mcp-egress-tools.ts +10 -9
  295. package/src/mcp-issue-activity-tools.ts +2 -2
  296. package/src/mcp-issue-association-tools.ts +4 -4
  297. package/src/mcp-issue-ref-tools.ts +3 -3
  298. package/src/mcp-merge-tools.ts +5 -5
  299. package/src/mcp-notification-subscription-tools.ts +14 -15
  300. package/src/mcp-output-levels.test-fixture.ts +1 -1
  301. package/src/mcp-profile-trust-tools.ts +1 -1
  302. package/src/mcp-project-journal-tools.ts +4 -12
  303. package/src/mcp-project-tools.ts +17 -18
  304. package/src/mcp-routing-policy-tools.ts +3 -2
  305. package/src/mcp-team-tools.ts +6 -6
  306. package/src/mcp-workflow-tools.ts +5 -3
  307. package/src/memory-area-injection.ts +13 -12
  308. package/src/memory-boundary-capture.ts +6 -6
  309. package/src/memory-injection.ts +45 -43
  310. package/src/merge-pause-maintenance.ts +5 -4
  311. package/src/notification-router.ts +44 -17
  312. package/src/notify.ts +5 -5
  313. package/src/observed-reset-refresh.ts +4 -4
  314. package/src/operator-alarm.ts +2 -5
  315. package/src/project-ingest.ts +6 -6
  316. package/src/project-instructions.ts +8 -1
  317. package/src/project-trust.ts +12 -11
  318. package/src/prompt-resolver.ts +3 -3
  319. package/src/provider-quota-async.ts +5 -7
  320. package/src/provider-timeline-event.ts +4 -7
  321. package/src/quota-band-delivery.ts +2 -2
  322. package/src/recipe-runner.ts +4 -3
  323. package/src/retention-config-store.ts +4 -3
  324. package/src/reviewer-pipeline.ts +11 -10
  325. package/src/routes/_route.ts +4 -4
  326. package/src/routes/_shared.ts +13 -12
  327. package/src/routes/agent-effort.ts +5 -5
  328. package/src/routes/agent-heartbeat-quota.ts +23 -0
  329. package/src/routes/agent-keep.ts +1 -1
  330. package/src/routes/agent-reporting.ts +1 -1
  331. package/src/routes/agent-self-effort.ts +5 -5
  332. package/src/routes/agent-sessions.ts +24 -24
  333. package/src/routes/agents-spawn.ts +1 -1
  334. package/src/routes/agents.ts +17 -119
  335. package/src/routes/auth.ts +14 -15
  336. package/src/routes/authority-audit.ts +2 -2
  337. package/src/routes/automations.ts +6 -5
  338. package/src/routes/channel-binding-delete.ts +1 -1
  339. package/src/routes/channel-output.ts +3 -3
  340. package/src/routes/claude-api-budget.ts +5 -5
  341. package/src/routes/commands-listing.ts +1 -1
  342. package/src/routes/commands.ts +29 -27
  343. package/src/routes/context-resets.ts +2 -2
  344. package/src/routes/coordinator-teams.ts +1 -1
  345. package/src/routes/decisions.ts +2 -2
  346. package/src/routes/host-secret-files.ts +1 -1
  347. package/src/routes/hosts.ts +11 -11
  348. package/src/routes/inbox.ts +3 -2
  349. package/src/routes/index.ts +2 -1
  350. package/src/routes/integrations.ts +9 -7
  351. package/src/routes/issue-activity.ts +2 -2
  352. package/src/routes/issue-associations.ts +4 -4
  353. package/src/routes/issue-refs.ts +3 -3
  354. package/src/routes/kernel-timers-operator.ts +1 -1
  355. package/src/routes/memory.ts +1 -1
  356. package/src/routes/merges.ts +11 -11
  357. package/src/routes/messages.ts +23 -17
  358. package/src/routes/orchestrator-bootstrap.ts +1 -1
  359. package/src/routes/orchestrator-host-registration.ts +2 -0
  360. package/src/routes/orchestrator-host-utilization.ts +2 -2
  361. package/src/routes/orchestrator-proxy.ts +2 -2
  362. package/src/routes/orchestrator.ts +27 -18
  363. package/src/routes/pairs.ts +8 -8
  364. package/src/routes/project-profile-instructions.ts +1 -1
  365. package/src/routes/project-trust.ts +1 -1
  366. package/src/routes/projects.ts +16 -16
  367. package/src/routes/prompt-templates.ts +12 -12
  368. package/src/routes/provider-quotas.ts +10 -10
  369. package/src/routes/run-command.ts +1 -1
  370. package/src/routes/secrets-backup.ts +2 -2
  371. package/src/routes/secrets-import.ts +1 -1
  372. package/src/routes/secrets-vault.ts +7 -7
  373. package/src/routes/settings.ts +1 -1
  374. package/src/routes/shipping.ts +9 -1
  375. package/src/routes/spawn-policy.ts +8 -7
  376. package/src/routes/sse.ts +2 -2
  377. package/src/routes/team-templates.ts +7 -7
  378. package/src/routes/teams.ts +10 -10
  379. package/src/routes/tokens.ts +18 -17
  380. package/src/routes/users.ts +24 -24
  381. package/src/routes/viewer-read.ts +3 -3
  382. package/src/routes/web-tool-calls.ts +4 -4
  383. package/src/routes/workflows.ts +2 -2
  384. package/src/routes/workspaces.ts +17 -16
  385. package/src/routing-policy-push.ts +6 -6
  386. package/src/runtime-grant.ts +17 -11
  387. package/src/runtime-tokens.ts +20 -28
  388. package/src/secrets-broker/config.ts +1 -1
  389. package/src/secrets-broker/service.ts +2 -2
  390. package/src/security.ts +3 -1
  391. package/src/self-resume-carry.ts +21 -110
  392. package/src/self-resume-supersede.ts +4 -3
  393. package/src/self-resume.ts +54 -66
  394. package/src/server-background.ts +1 -1
  395. package/src/server.ts +21 -22
  396. package/src/services/agent-action.ts +12 -31
  397. package/src/services/agent-effort.ts +7 -32
  398. package/src/services/agent-hang-watchdog-job.ts +2 -2
  399. package/src/services/agent-heartbeat.ts +166 -0
  400. package/src/services/agent-reporting.ts +1 -1
  401. package/src/services/browser-inventory.ts +3 -3
  402. package/src/services/browser-session-state.ts +3 -2
  403. package/src/services/browser-session.ts +21 -27
  404. package/src/services/bus-command-update.ts +9 -9
  405. package/src/services/checkout-refresh-state.ts +1 -0
  406. package/src/services/checkout-refresh.test-fixture.ts +3 -2
  407. package/src/services/command-host.ts +7 -2
  408. package/src/services/coordinator-digest.ts +3 -2
  409. package/src/services/dispatch-command.ts +12 -13
  410. package/src/services/effective-launch-driver.ts +14 -9
  411. package/src/services/host-changed-paths.test-fixture.ts +5 -6
  412. package/src/services/host-project-readiness.ts +2 -2
  413. package/src/services/host-providers.ts +1 -1
  414. package/src/services/host-utilization.ts +9 -9
  415. package/src/services/idle-agent-digest.ts +17 -12
  416. package/src/services/idle-epics.ts +4 -4
  417. package/src/services/interrupt-agent.ts +12 -9
  418. package/src/services/issue-activity.ts +3 -3
  419. package/src/services/issue-associations.ts +15 -18
  420. package/src/services/issue-lifecycle-comments.ts +3 -2
  421. package/src/services/issue-lifecycle.ts +14 -13
  422. package/src/services/issue-refs.ts +4 -5
  423. package/src/services/kernel-timers-cutover-chaos-worker.ts +1 -1
  424. package/src/services/kernel-timers-ingress-chaos-worker.ts +6 -1
  425. package/src/services/kernel-timers-ingress.ts +3 -3
  426. package/src/services/kernel-timers-message-authority-chaos-worker.ts +3 -10
  427. package/src/services/kernel-timers-operator-chaos-worker.ts +3 -2
  428. package/src/services/kernel-timers-outbox.ts +1 -1
  429. package/src/services/kernel-timers-stage10-operator.ts +1 -1
  430. package/src/services/kernel-timers.ts +4 -3
  431. package/src/services/land-gate-placement.ts +11 -2
  432. package/src/services/land-issue-relations.ts +5 -4
  433. package/src/services/land-restart-retry.ts +7 -6
  434. package/src/services/landed-ancestry.ts +6 -5
  435. package/src/services/legacy-landing-host.ts +4 -1
  436. package/src/services/legacy-landing-recovery.ts +8 -6
  437. package/src/services/managed-running.ts +17 -17
  438. package/src/services/memory-areas.ts +3 -3
  439. package/src/services/merge-pause.ts +9 -15
  440. package/src/services/operator-decisions.ts +37 -41
  441. package/src/services/orchestrator-registration-recovery.ts +6 -2
  442. package/src/services/orchestrator-upgrade.ts +14 -12
  443. package/src/services/permission-decision.ts +4 -3
  444. package/src/services/plan-ci-notice.ts +1 -1
  445. package/src/services/plan-conformance.ts +4 -4
  446. package/src/services/plan-dispatch-policy.ts +10 -3
  447. package/src/services/plan-dispatch.ts +35 -14
  448. package/src/services/plan-graph.ts +39 -261
  449. package/src/services/plan-import.ts +1 -1
  450. package/src/services/plan-live-bindings.ts +8 -37
  451. package/src/services/plan-task-create.ts +1 -1
  452. package/src/services/plan-terminal-refs.ts +2 -12
  453. package/src/services/plan-wake-digest.ts +8 -3
  454. package/src/services/profile-trust.ts +2 -2
  455. package/src/services/project.ts +11 -10
  456. package/src/services/prompt-templates.ts +6 -5
  457. package/src/services/provider-hold-nudge.ts +9 -9
  458. package/src/services/provider-hold-record.ts +13 -12
  459. package/src/services/provider-state-audit.ts +19 -18
  460. package/src/services/provisioning-harness/driver.ts +1 -1
  461. package/src/services/provisioning-harness/fixture-matrix.ts +3 -3
  462. package/src/services/provisioning-harness/lifecycle.ts +24 -22
  463. package/src/services/provisioning-harness/prepare.ts +16 -17
  464. package/src/services/rate-limit-resume.ts +10 -9
  465. package/src/services/red-main-auto-revert.ts +7 -5
  466. package/src/services/red-main-controller-deps.ts +20 -3
  467. package/src/services/red-main-incident.ts +3 -1
  468. package/src/services/red-main-quarantine.ts +2 -3
  469. package/src/services/register-agent.ts +22 -22
  470. package/src/services/restart-project-role.ts +1 -1
  471. package/src/services/restart-spawn.ts +17 -16
  472. package/src/services/review-publish.ts +7 -24
  473. package/src/services/review-ref-reconcile.ts +29 -38
  474. package/src/services/runtime-dependency-alert.ts +27 -0
  475. package/src/services/scheduler-command.ts +11 -9
  476. package/src/services/scheduler.ts +5 -5
  477. package/src/services/self-effort-grant.ts +5 -25
  478. package/src/services/send-message.ts +16 -33
  479. package/src/services/shutdown-agent.ts +17 -14
  480. package/src/services/spawn-agent.ts +12 -41
  481. package/src/services/spawn-chain-inspector.ts +1 -1
  482. package/src/services/spawn-cwd-placement.ts +21 -5
  483. package/src/services/spawn-issue-activity.ts +8 -6
  484. package/src/services/spawn-launch.ts +17 -1
  485. package/src/services/spawn-loss-escalation.ts +1 -1
  486. package/src/services/spawn-registration-watchdog.ts +2 -2
  487. package/src/services/spawn-repo-probe.ts +7 -5
  488. package/src/services/spawn-tree-cleanup.ts +7 -8
  489. package/src/services/steward-reaper.ts +4 -4
  490. package/src/services/task-context-hydration.ts +25 -1
  491. package/src/services/task-delivery.ts +20 -34
  492. package/src/services/task-entity.ts +40 -203
  493. package/src/services/task-issue-link.ts +13 -20
  494. package/src/services/task-issue-weave.ts +12 -11
  495. package/src/services/task-lifecycle.ts +27 -31
  496. package/src/services/task-repo-cwd.ts +37 -0
  497. package/src/services/task-review-hold.ts +4 -17
  498. package/src/services/task-semantic-events.ts +13 -11
  499. package/src/services/task-state-reconcile.ts +3 -4
  500. package/src/services/task-validation.ts +2 -2
  501. package/src/services/team-templates.ts +5 -5
  502. package/src/services/team.ts +31 -98
  503. package/src/services/terminal-provider-exit.ts +6 -5
  504. package/src/services/timer-message-fire-chaos-worker.ts +7 -3
  505. package/src/services/timer-message-fire-outbox.ts +1 -1
  506. package/src/services/transient-agent-reaper.ts +22 -19
  507. package/src/services/turn-reconcile-audit.ts +9 -9
  508. package/src/services/user-onboarding.ts +8 -6
  509. package/src/services/user-profile.ts +6 -9
  510. package/src/services/work-finality.ts +16 -4
  511. package/src/services/workflow-auto-start.ts +2 -1
  512. package/src/services/workflow-caller.ts +61 -4
  513. package/src/services/workflow-child-proof-worker.ts +1 -1
  514. package/src/services/workflow-control-proof-worker.ts +1 -1
  515. package/src/services/workflow-coordinator.ts +8 -2
  516. package/src/services/workflow-decisions.ts +5 -5
  517. package/src/services/workflow-deferred-cleanup.ts +1 -1
  518. package/src/services/workflow-effect-driver.ts +42 -19
  519. package/src/services/workflow-effects.ts +41 -27
  520. package/src/services/workflow-pins.ts +7 -4
  521. package/src/services/workflow-reconcile.ts +1 -0
  522. package/src/services/workflow-resume-pins.ts +3 -3
  523. package/src/services/workflow-routing-defaults.ts +18 -0
  524. package/src/services/workflow-run-view.ts +4 -3
  525. package/src/services/workflow-run.test-fixture.ts +1 -1
  526. package/src/services/workflow-runs.ts +34 -22
  527. package/src/services/workflow-supervisor-state.ts +21 -1
  528. package/src/services/workflow-supervisor.ts +4 -3
  529. package/src/services/workspace-recovery.ts +4 -3
  530. package/src/spawn-admission.ts +10 -2
  531. package/src/spawn-placement.ts +27 -9
  532. package/src/spawn-targets.ts +8 -8
  533. package/src/sse-agent-status.ts +15 -6
  534. package/src/sse.ts +49 -24
  535. package/src/startup-context-injection.ts +61 -57
  536. package/src/steward-handoff.ts +4 -3
  537. package/src/steward-policy-cleanup.ts +9 -8
  538. package/src/steward.ts +2 -2
  539. package/src/task-dispatch-live.ts +5 -2
  540. package/src/task-dispatcher.ts +2 -2
  541. package/src/token-db.ts +2 -2
  542. package/src/token-usage-async.ts +2 -4
  543. package/src/user-awaiting-reply.ts +5 -5
  544. package/src/workspace-actions.ts +24 -29
  545. package/src/workspace-auto-merge.ts +14 -12
  546. package/src/workspace-changed-paths-client.ts +16 -5
  547. package/src/workspace-cleanup-stale.ts +30 -7
  548. package/src/workspace-command-settle.ts +24 -16
  549. package/src/workspace-config.ts +1 -1
  550. package/src/workspace-conflict-resolver.ts +9 -7
  551. package/src/workspace-dead-owner-recovery.ts +5 -4
  552. package/src/workspace-escalation.ts +12 -11
  553. package/src/workspace-human-escalation.ts +3 -3
  554. package/src/workspace-land-gate-watchdog.ts +3 -3
  555. package/src/workspace-land-refusal.ts +20 -0
  556. package/src/workspace-land-stopped.ts +1 -0
  557. package/src/workspace-merge.ts +10 -26
  558. package/src/workspace-nothing-to-land-notice.ts +2 -2
  559. package/src/workspace-orphans.ts +18 -37
  560. package/src/workspace-pr-completion.ts +10 -8
  561. package/src/workspace-rekey.ts +3 -3
  562. package/src/workspace-steward-failure.ts +2 -2
  563. package/src/workspace-steward-wake.ts +2 -2
  564. package/public/assets/agent-liveness-CaNwK0Zf.js +0 -2
  565. package/public/assets/provider-display-DsuWMvVL.js +0 -2
  566. package/public/assets/run-card-CpKqRbhg.js +0 -2
  567. package/public/assets/shipping-B9YtnwtG.js +0 -2
  568. package/public/assets/store-DcClXLH0.js +0 -9
  569. package/runner/src/agent-runner/http-liveness.ts +0 -85
  570. package/src/db/on-loop-write-transaction.ts +0 -34
@@ -18,7 +18,7 @@ import { launchWorkflowRun, prepareWorkflowRun } from "./workflow-runs";
18
18
  import { ensureSupervisor, invoke, type Supervisor } from "./workflow-supervisor";
19
19
  import { stampPins } from "./workflow-pins";
20
20
  import { emitPlan } from "./plan-graph";
21
- import { dependencyStage, memberReady, occupiedSlots, planMembers, prerequisiteState, slotFromRun, taskSucceeded, unsupportedPlanShape } from "./plan-dispatch-policy";
21
+ import { dependencyStage, memberReady, occupiedSlots, planMembers, planNodeSettled, prerequisiteState, slotFromRun, taskSucceeded, unsupportedPlanShape } from "./plan-dispatch-policy";
22
22
  import type { AuthContext } from "./auth-context";
23
23
  import { planSlotOccupies, type Plan, type PlanDispatch, type PlanDispatchSlot, type TaskDetail } from "../types";
24
24
 
@@ -44,9 +44,13 @@ export async function startPlanDispatch(input: { planId?: unknown; expectedVersi
44
44
  if (plan.archived) throw new ValidationError(`plan ${plan.id} is archived`);
45
45
  const problems = unsupportedPlanShape(plan);
46
46
  if (problems.length) throw new ValidationError(problems.join("; "));
47
- const members = planMembers(plan);
48
- const tasks = await Promise.all(members.map((member) => getTaskDetailAsync(member.taskId)));
49
- const memberIds = new Set(members.map((member) => member.taskId));
47
+ const nodes = new Map(plan.nodes.map((node) => [node.id, node]));
48
+ const allMembers = planMembers(plan);
49
+ const allTasks = await Promise.all(allMembers.map((member) => getTaskDetailAsync(member.taskId)));
50
+ const unfinished = allMembers.flatMap((member, index) => planNodeSettled(nodes.get(member.nodeId)!, allTasks[index]!) ? [] : [{ member, task: allTasks[index]! }]);
51
+ const members = unfinished.map(({ member }) => member);
52
+ const tasks = unfinished.map(({ task }) => task);
53
+ const memberIds = new Set(plan.nodes.map((node) => Number(node.refs?.taskId)));
50
54
  for (const member of tasks) {
51
55
  for (const id of member?.dependsOn ?? []) {
52
56
  if (memberIds.has(id)) continue;
@@ -55,7 +59,7 @@ export async function startPlanDispatch(input: { planId?: unknown; expectedVersi
55
59
  }
56
60
  }
57
61
  const repos = new Set(tasks.map((task) => task?.repo).filter(Boolean));
58
- if (repos.size !== 1) throw new ValidationError(`UNSUPPORTED_PLAN_SHAPE: slice 1a dispatches one repo; the plan's Tasks name ${repos.size ? [...repos].join(", ") : "no repo"}`);
62
+ if (tasks.length && repos.size !== 1) throw new ValidationError(`UNSUPPORTED_PLAN_SHAPE: slice 1a dispatches one repo; the plan's Tasks name ${repos.size ? [...repos].join(", ") : "no repo"}`);
59
63
  let cwd: string | undefined;
60
64
  for (const [index, task] of tasks.entries()) {
61
65
  if (!task || task.status === "canceled") throw new ValidationError(`UNSUPPORTED_PLAN_SHAPE: T${members[index]!.taskId} is missing or canceled`);
@@ -115,6 +119,8 @@ function ensureListening(): void {
115
119
  if (event.type === "task.stage" || event.type === "task.status" || event.type === "task.updated") {
116
120
  const plans = watched.get(Number(event.data.id ?? event.subject));
117
121
  if (plans?.size) void fireAndForget("[plan-dispatch] reconcile", () => requestReconcile(plans));
122
+ } else if (event.type === "plan.changed" && event.subject && dispatching.has(event.subject)) {
123
+ void fireAndForget("[plan-dispatch] reconcile", () => requestReconcile([event.subject!]));
118
124
  } else if (dispatching.size && (event.type === "workflow.changed" || event.type === "branch.landed" || event.type === "ci.status")) {
119
125
  void fireAndForget("[plan-dispatch] reconcile", () => requestReconcile());
120
126
  }
@@ -184,9 +190,22 @@ async function reconcilePlan(planId: string): Promise<void> {
184
190
  changed ||= next !== slot;
185
191
  slots.set(slot.taskId, next);
186
192
  }
193
+ const nodes = new Map(plan.nodes.map((node) => [node.id, node]));
194
+ const nodesForTask = new Map(plan.nodes.filter((node) => node.refs?.taskId).map((node) => [Number(node.refs!.taskId), node]));
195
+ const prerequisites = new Map<string, string[]>();
196
+ for (const edge of plan.edges) {
197
+ if (!prerequisites.has(edge.to)) prerequisites.set(edge.to, []);
198
+ prerequisites.get(edge.to)!.push(edge.from);
199
+ }
200
+ const settledNode = async (nodeId: string, requiredStage?: Parameters<typeof taskSucceeded>[2]) => {
201
+ const node = nodes.get(nodeId)!;
202
+ const id = Number(node.refs?.taskId);
203
+ return planNodeSettled(node, Number.isSafeInteger(id) && id > 0 ? await task(id) : null, slots.get(id), requiredStage);
204
+ };
187
205
  if (dispatch.state === "active") {
188
206
  for (const { nodeId, taskId } of members) {
189
207
  const slot = slots.get(taskId);
208
+ if (!slot && await settledNode(nodeId)) continue;
190
209
  if (slot && (slot.status !== "launch-failed" && slot.status !== "failed" && slot.status !== "cancelled")) {
191
210
  const dispatched = planSlotOccupies(slot.status) ? await task(taskId) : null;
192
211
  if (dispatched) await escalateRegressions(plan, nodeId, dispatched, memberTaskIds, task);
@@ -207,14 +226,16 @@ async function reconcilePlan(planId: string): Promise<void> {
207
226
  for (const id of member.dependsOn) {
208
227
  const prerequisite = await task(id);
209
228
  const required = dependencyStage(member, id);
210
- if (taskSucceeded(prerequisite, slots.get(id), required)) succeeded.add(id);
229
+ const node = nodesForTask.get(id);
230
+ if (node ? await settledNode(node.id, required) : taskSucceeded(prerequisite, slots.get(id), required)) succeeded.add(id);
211
231
  else {
212
232
  const state = prerequisiteState(prerequisite, required);
213
233
  if (state.state === "unreachable") {
214
- notifyPlan(plan, `T${taskId} prerequisite unreachable`, `Plan ${plan.id}, node ${nodeId} (T${taskId}) waits on T${id}, which is ${state.reason}. Replace the edge with relay_task_remove_dependency then relay_task_add_dependency, cancel the node, or wait for T${id} to be reopened. The dependent keeps waiting.`, `plan:${plan.id}:prereq:T${id}:${state.reason}`, { kind: "plan.prerequisite-unreachable", planId, nodeId, taskId, prerequisiteTaskId: id, reason: state.reason, nextActions: ["replace-edge", "cancel-node", "wait-for-reopen"] });
234
+ await notifyPlan(plan, `T${taskId} prerequisite unreachable`, `Plan ${plan.id}, node ${nodeId} (T${taskId}) waits on T${id}, which is ${state.reason}. Replace the edge with relay_task_remove_dependency then relay_task_add_dependency, cancel the node, or wait for T${id} to be reopened. The dependent keeps waiting.`, `plan:${plan.id}:prereq:T${id}:${state.reason}`, { kind: "plan.prerequisite-unreachable", planId, nodeId, taskId, prerequisiteTaskId: id, reason: state.reason, nextActions: ["replace-edge", "cancel-node", "wait-for-reopen"] });
215
235
  }
216
236
  }
217
237
  }
238
+ if (!(await Promise.all((prerequisites.get(nodeId) ?? []).map((id) => settledNode(id)))).every(Boolean)) continue;
218
239
  if (!memberReady(member, undefined, (id) => succeeded.has(id))) continue;
219
240
  if (occupiedSlots(slots.values()) >= dispatch.concurrency) continue;
220
241
  const claimed = await write<{ slot?: PlanDispatchSlot; reserved: boolean }>("planDispatch.slot.reserve", { planId, taskId, nodeId });
@@ -223,7 +244,7 @@ async function reconcilePlan(planId: string): Promise<void> {
223
244
  if (fault === "after-reserve") { halted = true; throw new Error("plan dispatch test fault: the process stopped after reserving a slot"); }
224
245
  slots.set(taskId, await launch(plan, claimed.slot));
225
246
  }
226
- const settled = await Promise.all(members.map(async ({ taskId }) => taskSucceeded(await task(taskId), slots.get(taskId))));
247
+ const settled = await Promise.all(plan.nodes.map((node) => settledNode(node.id)));
227
248
  if (settled.every(Boolean)) {
228
249
  await openAudit(plan, dispatch, members.map(({ taskId }) => tasks.get(taskId)).find(Boolean) ?? null);
229
250
  changed = true;
@@ -245,7 +266,7 @@ async function escalateRegressions(plan: Plan, nodeId: string, member: TaskDetai
245
266
  const state = prerequisiteState(prerequisite, required);
246
267
  if (state.state === "satisfied") continue;
247
268
  const now = prerequisite ? `status ${prerequisite.status}, stage ${prerequisite.deliverableStage ?? "none"}${state.state === "unreachable" ? `, ${state.reason}` : ""}` : "missing";
248
- notifyPlan(plan, `T${member.id} prerequisite regressed`, `Plan ${plan.id}, node ${nodeId} (T${member.id}) was dispatched on T${id} reaching ${required}, which no longer holds (${now}). The run continues; check whether its work is still valid, or cancel the node.`, `plan:${plan.id}:prereq:T${id}:regressed`, { kind: "plan.prerequisite-unreachable", planId: plan.id, nodeId, taskId: member.id, prerequisiteTaskId: id, reason: "regressed", requiredStage: required, nextActions: ["check-run", "cancel-node", "wait-for-recovery"] });
269
+ await notifyPlan(plan, `T${member.id} prerequisite regressed`, `Plan ${plan.id}, node ${nodeId} (T${member.id}) was dispatched on T${id} reaching ${required}, which no longer holds (${now}). The run continues; check whether its work is still valid, or cancel the node.`, `plan:${plan.id}:prereq:T${id}:regressed`, { kind: "plan.prerequisite-unreachable", planId: plan.id, nodeId, taskId: member.id, prerequisiteTaskId: id, reason: "regressed", requiredStage: required, nextActions: ["check-run", "cancel-node", "wait-for-recovery"] });
249
270
  }
250
271
  }
251
272
 
@@ -279,7 +300,7 @@ async function launch(plan: Plan, slot: PlanDispatchSlot): Promise<PlanDispatchS
279
300
  const started = await launchWorkflowRun(prepared, { ...(inputs ? { inputs } : {}), ...(pins ? { pins } : {}), requestKey: `plan:${plan.id}:T${task.id}:a${slot.attempt}` }, AUTOPILOT_CTX);
280
301
  return (await write<PlanDispatchSlot | undefined>("planDispatch.slot.update", { taskId: slot.taskId, attempt: slot.attempt, runId: String(started.runId), status: String(started.status) })) ?? slot;
281
302
  } catch (error) {
282
- notifyPlan(plan, `T${slot.taskId} could not start`, `Plan ${plan.id} could not start T${slot.taskId}'s workflow: ${errMessage(error)}. Its slot is free and its dependents wait. Fix the cause, then start its workflow by hand (relay_workflow_start); the plan adopts that run.`, `plan:${plan.id}:launch-failed:T${slot.taskId}:a${slot.attempt}`, { kind: "plan.dispatch-failed", planId: plan.id, taskId: slot.taskId });
303
+ await notifyPlan(plan, `T${slot.taskId} could not start`, `Plan ${plan.id} could not start T${slot.taskId}'s workflow: ${errMessage(error)}. Its slot is free and its dependents wait. Fix the cause, then start its workflow by hand (relay_workflow_start); the plan adopts that run.`, `plan:${plan.id}:launch-failed:T${slot.taskId}:a${slot.attempt}`, { kind: "plan.dispatch-failed", planId: plan.id, taskId: slot.taskId });
283
304
  return (await write<PlanDispatchSlot | undefined>("planDispatch.slot.update", { taskId: slot.taskId, attempt: slot.attempt, status: "launch-failed", error: errMessage(error).slice(0, 2000) })) ?? slot;
284
305
  }
285
306
  }
@@ -310,7 +331,7 @@ async function driveAudit(sup: Supervisor, plan: Plan): Promise<boolean> {
310
331
  const started = await launchWorkflowRun(prepared, { requestKey: `plan:${plan.id}:audit` }, AUTOPILOT_CTX);
311
332
  await write("planDispatch.audit.update", { planId: plan.id, runId: String(started.runId), status: String(started.status) });
312
333
  } catch (error) {
313
- notifyPlan(plan, "audit could not start", `Plan ${plan.id}: every node landed, but its conformance audit (T${audit.taskId}) could not start: ${errMessage(error)}. Start ${PLAN_CONFORMANCE_WORKFLOW} on T${audit.taskId} by hand, or audit and record relay_plan_conformance yourself.`, `plan:${plan.id}:audit-failed`, { kind: "plan.dispatch-failed", planId: plan.id, taskId: audit.taskId });
334
+ await notifyPlan(plan, "audit could not start", `Plan ${plan.id}: every node landed, but its conformance audit (T${audit.taskId}) could not start: ${errMessage(error)}. Start ${PLAN_CONFORMANCE_WORKFLOW} on T${audit.taskId} by hand, or audit and record relay_plan_conformance yourself.`, `plan:${plan.id}:audit-failed`, { kind: "plan.dispatch-failed", planId: plan.id, taskId: audit.taskId });
314
335
  await write("planDispatch.audit.update", { planId: plan.id, status: "launch-failed" });
315
336
  }
316
337
  return true;
@@ -324,14 +345,14 @@ async function driveAudit(sup: Supervisor, plan: Plan): Promise<boolean> {
324
345
  dispatching.delete(plan.id);
325
346
  watchPlan(plan.id, new Set());
326
347
  const slots = plan.dispatch!.slots.map((slot) => `${slot.nodeId} T${slot.taskId} ${slot.runId ?? "no run"} ${slot.landedSha?.slice(0, 12) ?? ""}`.trim());
327
- notifyPlan(plan, "done", [`Plan ${plan.id}${plan.title ? ` "${plan.title}"` : ""} is done: conformance passed on ${audit.mainSha} at version ${plan.version}.`, ...slots, `Audit T${audit.taskId} run ${audit.runId}.`].join("\n"), `plan:${plan.id}:result:v${plan.version}`, { kind: "plan.result", planId: plan.id, planVersion: plan.version, mainSha: audit.mainSha });
348
+ await notifyPlan(plan, "done", [`Plan ${plan.id}${plan.title ? ` "${plan.title}"` : ""} is done: conformance passed on ${audit.mainSha} at version ${plan.version}.`, ...slots, `Audit T${audit.taskId} run ${audit.runId}.`].join("\n"), `plan:${plan.id}:result:v${plan.version}`, { kind: "plan.result", planId: plan.id, planVersion: plan.version, mainSha: audit.mainSha });
328
349
  }
329
350
  return true;
330
351
  }
331
352
 
332
- function notifyPlan(plan: Plan, subject: string, body: string, idempotencyKey: string, payload: Json): void {
353
+ async function notifyPlan(plan: Plan, subject: string, body: string, idempotencyKey: string, payload: Json): Promise<void> {
333
354
  const to = planNoticeTarget(plan);
334
- if (to) notifySystemMessage(to, { subject: `Plan ${plan.title ?? plan.id}: ${subject}`, body, idempotencyKey, replyExpected: false, payload });
355
+ if (to) await notifySystemMessage(to, { subject: `Plan ${plan.title ?? plan.id}: ${subject}`, body, idempotencyKey, replyExpected: false, payload });
335
356
  }
336
357
 
337
358
  // --- serving ---------------------------------------------------------------------------------------
@@ -1,11 +1,14 @@
1
+ import { resolveIssueRefFromStringAsync } from "../db/issue-refs-async";
2
+ import { removeAssociationAsync, associateIssueAsync } from "../db/runtime-tail-async";
3
+ import { deletePlanAsync, createPlanRowAsync } from "../db/plans-async";
1
4
  import { hasScope, isRecord } from "@humwick/sdk";
2
- import { addTaskDependency, createPlanRow, deletePlan, getAgent, getEntry, getPlan, getPlanAsync, getPlanByTeam, getPlanByThread, getTaskDetail, listPlansWithBoundOwner, planThreadExists, removeTaskDependency, replacePlanNodeScoped, sendMessage, sendMessageWithResultAsync, setPlanArchivedAsync, ValidationError } from "../db";
5
+ import { sendMessageAsync, addTaskDependency, getAgent, getEntry, getPlan, getPlanAsync, getPlanByTeam, getPlanByThread, getTaskDetail, listPlansWithBoundOwner, planThreadExists, removeTaskDependency, replacePlanNodeScoped, sendMessageWithResultAsync, setPlanArchivedAsync, ValidationError } from "../db";
3
6
  // The shared #2520 patch-time source derivation lives beside the writer op body it mirrors;
4
7
  // plan-graph op bodies are imported directly (the barrel only re-exports plan-graph-async).
5
8
  import { applyPatchStatusSourceDerivation, bindPlanNodeBody } from "../db/plan-graph";
6
9
  import { writeTransaction } from "../db/connection";
7
- import { associateIssue, planNodeIssueEntityId, removeAssociation } from "../db/issue-associations";
8
- import { resolveIssueRefFromString } from "../db/issue-refs";
10
+ import { planNodeIssueEntityId } from "../db/issue-associations";
11
+
9
12
  import { emitRelayEvent } from "../events";
10
13
  import { cleanString, cleanStringArray, optionalEnum } from "../validation";
11
14
  import { resolveActorIssueRepo } from "../agent-issue-repo";
@@ -84,67 +87,6 @@ function cleanOwnerRef(value: unknown): PlanOwnerRef | undefined {
84
87
  return Object.keys(out).length ? out : undefined;
85
88
  }
86
89
 
87
- // #220: a bare `#NNN` issueId ref needs a default repo to resolve against — issue-refs.ts
88
- // throws without one, contrary to its own error text advertising the bare form as accepted.
89
- // Resolve it the same way #533 resolves chat `#NNN` linkify: the CALLING agent's own project
90
- // (cwd → project → issueRepo), falling back to the single global env default. This scopes the
91
- // default to the actor's actual repo rather than guessing from teamId — agents have no
92
- // team→project FK; cwd is the only durable link (see agent-issue-repo.ts).
93
- // Shared with the Task issue-link path (#2214) via resolveActorIssueRepo.
94
- function cleanRefs(value: unknown, actor: string): PlanNode["refs"] | undefined {
95
- if (value === undefined || value === null) return undefined;
96
- const ref = cleanRecordRef(value, "refs");
97
- if (!ref) return undefined;
98
- const out: PlanNode["refs"] = {};
99
- const issueId = cleanString(ref.issueId, "refs.issueId", { max: 1000 });
100
- if (issueId) {
101
- out.issueId = resolveIssueRefFromString({ ref: issueId, defaultRepo: resolveActorIssueRepo(actor) }).id;
102
- }
103
- // #836: refs.taskId is the live binding seam to a durable Task. Validate it resolves to a
104
- // deliverable Task; store the canonical numeric id as a string. Once bound, the node's
105
- // label/scopeBlurb/status PROJECT from the Task (see projectPlanOverTasks) — no double-keeping.
106
- const taskIdRaw = ref.taskId;
107
- const taskIdStr = typeof taskIdRaw === "number" ? String(taskIdRaw) : cleanString(taskIdRaw, "refs.taskId", { max: 240 });
108
- if (taskIdStr) {
109
- const taskId = parseTaskId(taskIdStr);
110
- if (taskId === undefined) throw new ValidationError("refs.taskId must be a positive task id");
111
- if (!getTaskDetail(taskId)) throw new ValidationError(`refs.taskId ${taskId} is not a deliverable task`);
112
- out.taskId = String(taskId);
113
- }
114
- // agentId/landingRef remain populated by live binding in later slices (#838); reject here.
115
- const unsupported = ["agentId", "landingRef"].filter((key) => cleanString(ref[key], `refs.${key}`, { max: 240 }));
116
- if (unsupported.length > 0) throw new ValidationError("agentId/landingRef refs are populated by live binding in a later slice");
117
- return Object.keys(out).length ? out : undefined;
118
- }
119
-
120
- function syncPlanNodeIssueAssociation(planId: string, node: PlanNode): void {
121
- if (!node.refs?.issueId) return;
122
- associateIssue(node.refs.issueId, "plan-node", planNodeIssueEntityId(planId, node.id));
123
- }
124
-
125
- function removePlanNodeIssueAssociation(planId: string, node: PlanNode): void {
126
- if (!node.refs?.issueId) return;
127
- removeAssociation(node.refs.issueId, "plan-node", planNodeIssueEntityId(planId, node.id));
128
- }
129
-
130
- function syncChangedPlanNodeIssueAssociation(planId: string, before: PlanNode | undefined, after: PlanNode): void {
131
- if (before?.refs?.issueId && before.refs.issueId !== after.refs?.issueId) {
132
- removeAssociation(before.refs.issueId, "plan-node", planNodeIssueEntityId(planId, before.id));
133
- }
134
- if (after.refs?.issueId) syncPlanNodeIssueAssociation(planId, after);
135
- }
136
-
137
- function cleanBlackboardRefs(value: unknown, planTeamId?: string): string[] | undefined {
138
- const refs = cleanStringArray(value, "node.blackboardRefs", { itemMax: 120, maxItems: 100 });
139
- if (!refs?.length) return refs;
140
- if (!planTeamId) throw new ValidationError("plan has no team; cannot reference blackboard entries");
141
- for (const ref of refs) {
142
- const entry = getEntry(ref);
143
- if (!entry || entry.teamId !== planTeamId) throw new ValidationError(`blackboard entry not found for plan team: ${ref}`);
144
- }
145
- return refs;
146
- }
147
-
148
90
  function cleanStatusSource(value: unknown): PlanNode["statusSource"] {
149
91
  const statusSource = optionalEnum(value, "statusSource", ["manual", "bound"] as const, "manual");
150
92
  return statusSource;
@@ -181,27 +123,6 @@ function applyBoundStatus(node: PlanNode): PlanNode {
181
123
  return { ...node, status: boundStatusForAgent(getAgent(agentId)) ?? "pending" };
182
124
  }
183
125
 
184
- function cleanNode(input: PlanNodeInput, actor: string, planTeamId?: string): PlanNode {
185
- if (!isRecord(input)) throw new ValidationError("node must be an object");
186
- const scopeBlurb = cleanString(input.scopeBlurb, "node.scopeBlurb", { max: 2000 });
187
- const ownerRef = cleanOwnerRef(input.ownerRef);
188
- const lane = cleanString(input.lane, "node.lane", { max: 120 });
189
- const refs = cleanRefs(input.refs, actor);
190
- const blackboardRefs = cleanBlackboardRefs(input.blackboardRefs, planTeamId);
191
- const statusSource = deriveStatusSource(input.statusSource, input.status, Boolean(refs?.taskId || refs?.issueId));
192
- return applyBoundStatus({
193
- id: cleanString(input.id, "node.id", { required: true, max: 120 })!,
194
- label: cleanString(input.label, "node.label", { required: true, max: PLAN_NODE_LABEL_MAX })!,
195
- ...(scopeBlurb ? { scopeBlurb } : {}),
196
- ...(ownerRef ? { ownerRef } : {}),
197
- ...(lane ? { lane } : {}),
198
- status: optionalEnum(input.status, "node.status", PLAN_STATUSES, "pending") as PlanStatus,
199
- statusSource,
200
- ...(refs ? { refs } : {}),
201
- ...(blackboardRefs ? { blackboardRefs } : {}),
202
- });
203
- }
204
-
205
126
  // The aggregate writer owns persisted validation (issue/task/blackboard existence and bound
206
127
  // presence). This companion only validates shape, producing clone-safe command data.
207
128
  function cleanNodeForAggregate(input: PlanNodeInput): PlanNode {
@@ -247,14 +168,6 @@ function cleanPatchForAggregate(patch: PlanNodePatch): Partial<PlanNode> {
247
168
  return out;
248
169
  }
249
170
 
250
- function cleanNodes(value: unknown, actor: string, planTeamId?: string): PlanNode[] {
251
- if (value === undefined || value === null) return [];
252
- if (!Array.isArray(value)) throw new ValidationError("nodes must be an array");
253
- const nodes = value.map((item) => cleanNode(item as PlanNodeInput, actor, planTeamId));
254
- assertUnique(nodes.map((node) => node.id), "node id");
255
- return nodes;
256
- }
257
-
258
171
  function cleanEdge(input: unknown): PlanEdge {
259
172
  if (!isRecord(input)) throw new ValidationError("edge must be an object");
260
173
  return {
@@ -340,19 +253,19 @@ export function emitPlan(type: "plan.created" | "plan.changed" | "plan.deleted",
340
253
 
341
254
  // #1035: a plan anchors to a messages.thread_id, but a fresh coordinator has no thread yet —
342
255
  // forcing it to send a throwaway message just to mint one. When no threadId is supplied, mint the
343
- // thread server-side by self-threading a lightweight system message (the same sendMessage path the
256
+ // thread server-side by self-threading a lightweight system message (the same sendMessageAsync path the
344
257
  // caller would have used). Sent AS the actor when that agent is registered, else the reserved
345
258
  // `system` sink, so the mint always passes the sender-registration gate.
346
- function mintPlanThread(ctx: AuthContext, channel: string | undefined): number {
259
+ async function mintPlanThread(ctx: AuthContext, channel: string | undefined): Promise<number> {
347
260
  ensureCanMintThread(ctx);
348
261
  const actor = actorId(ctx);
349
262
  const sender = getAgent(actor) ? actor : "system";
350
- const root = sendMessage({
263
+ const root = (await sendMessageAsync({
351
264
  from: sender,
352
265
  to: "system",
353
266
  body: "Plan thread root",
354
267
  ...(channel ? { channel } : {}),
355
- });
268
+ }));
356
269
  return root.threadId ?? root.id;
357
270
  }
358
271
 
@@ -364,7 +277,7 @@ async function mintPlanThreadAsync(ctx: AuthContext, channel: string | undefined
364
277
  return root.threadId ?? root.id;
365
278
  }
366
279
 
367
- // #1043: the mint inserts a real message row through the db `sendMessage` path, which normally
280
+ // #1043: the mint inserts a real message row through the db `sendMessageAsync` path, which normally
368
281
  // lives behind the `message:send` scope. `plans:write` alone must not be a side-door around it, so
369
282
  // gate the mint on `message:send`. Internal `system` principals (empty-scope trusted callers) and
370
283
  // full-reach `*` tokens pass through untouched — `hasScope` short-circuits `*`.
@@ -375,78 +288,16 @@ function ensureCanMintThread(ctx: AuthContext): void {
375
288
  }
376
289
  }
377
290
 
378
- export function createPlan(input: PlanCreateInput, ctx: AuthContext): Plan {
379
- const channel = cleanString(input.channel, "channel", { max: 120 });
380
- const actor = actorId(ctx);
381
- const explicitTeamId = cleanString(input.teamId, "teamId", { max: 120 });
382
- const teamId = explicitTeamId ?? (input.teamId === undefined || input.teamId === null ? getAgent(actor)?.teamId : undefined);
383
- const nodes = cleanNodes(input.nodes, actor, teamId);
384
- const edges = cleanEdges(input.edges);
385
- validateEdges(nodes, edges);
386
- const title = cleanString(input.title, "title", { max: 240 });
387
- const objective = cleanString(input.objective, "objective", { max: 4000 });
388
- const acceptance = cleanPlanAcceptance(input.acceptance);
389
- // Resolve the thread only AFTER the payload validates (#1043): auto-minting first left an orphan
390
- // 'Plan thread root' row behind on every rejected create.
391
- const threadId = input.threadId === undefined || input.threadId === null
392
- ? mintPlanThread(ctx, channel)
393
- : cleanThreadId(input.threadId);
394
- if (!planThreadExists(threadId)) throw new ValidationError(`thread ${threadId} not found`);
395
- const plan = createPlanRow({
396
- threadId,
397
- teamId,
398
- channel,
399
- title,
400
- objective,
401
- nodes,
402
- edges,
403
- acceptance,
404
- createdBy: actor,
405
- });
406
- for (const node of plan.nodes) syncPlanNodeIssueAssociation(plan.id, node);
407
- emitPlan("plan.created", plan, actor);
408
- return plan;
291
+ export async function createPlan(input: PlanCreateInput, ctx: AuthContext): Promise<Plan> {
292
+ return createPlanAsync(input, ctx);
409
293
  }
410
294
 
411
- export function addPlanNode(planId: string, nodeInput: PlanNodeInput, ctx: AuthContext): Plan {
412
- const actor = actorId(ctx);
413
- // S6 #2079: fence behind the version-CAS; re-derive the node on the freshest snapshot each attempt.
414
- let node!: PlanNode;
415
- const updated = replacePlanNodeScoped(planId, (plan) => {
416
- node = cleanNode(nodeInput, actor, plan.teamId);
417
- if (plan.nodes.some((item) => item.id === node.id)) throw new ValidationError(`node ${node.id} already exists`);
418
- return { ...plan, nodes: [...plan.nodes, node] };
419
- }, actor);
420
- syncPlanNodeIssueAssociation(updated.id, node); emitPlan("plan.changed", updated, actor);
421
- return updated;
422
- }
423
-
424
- export function updatePlanNode(planId: string, nodeId: string, patch: PlanNodePatch, ctx: AuthContext): Plan {
425
- const actor = actorId(ctx);
426
- // S6 #2079: re-apply the patch onto the latest node map (by nodeId); a distinct-node edit survives.
427
- let existing!: PlanNode; let next!: PlanNode;
428
- const updated = replacePlanNodeScoped(planId, (plan) => {
429
- const current = plan.nodes.find((node) => node.id === nodeId);
430
- if (!current) throw new ValidationError(`node ${nodeId} not found`); existing = current;
431
- const statusSource = patch.statusSource !== undefined ? cleanStatusSource(patch.statusSource) : current.statusSource;
432
- const base: PlanNode = {
433
- ...current,
434
- ...(patch.label !== undefined ? { label: cleanString(patch.label, "label", { required: true, max: PLAN_NODE_LABEL_MAX })! } : {}),
435
- ...(patch.scopeBlurb !== undefined ? optionalProp("scopeBlurb", cleanString(patch.scopeBlurb, "scopeBlurb", { max: 2000 })) : {}),
436
- ...(patch.ownerRef !== undefined ? optionalProp("ownerRef", cleanOwnerRef(patch.ownerRef)) : {}),
437
- ...(patch.lane !== undefined ? optionalProp("lane", cleanString(patch.lane, "lane", { max: 120 })) : {}),
438
- ...(patch.status !== undefined ? { status: optionalEnum(patch.status, "status", PLAN_STATUSES) as PlanStatus } : {}),
439
- ...(patch.refs !== undefined ? optionalProp("refs", cleanRefs(patch.refs, actor)) : {}),
440
- ...(patch.blackboardRefs !== undefined ? optionalProp("blackboardRefs", cleanBlackboardRefs(patch.blackboardRefs, plan.teamId)) : {}),
441
- statusSource,
442
- };
443
- // #2520 RESOLUTION — the ONE shared patch-time source derivation (lives beside the writer op
444
- // body it mirrors; see applyPatchStatusSourceDerivation in db/plan-graph.ts).
445
- next = applyBoundStatus(applyPatchStatusSourceDerivation(patch, current, base));
446
- return { ...plan, nodes: plan.nodes.map((node) => node.id === nodeId ? next : node) };
447
- }, actor);
448
- syncChangedPlanNodeIssueAssociation(updated.id, existing, next); emitPlan("plan.changed", updated, actor);
449
- return updated;
295
+ export async function addPlanNode(planId: string, nodeInput: PlanNodeInput, ctx: AuthContext): Promise<Plan> {
296
+ return addPlanNodeAsync(planId, nodeInput, ctx);
297
+ }
298
+
299
+ export async function updatePlanNode(planId: string, nodeId: string, patch: PlanNodePatch, ctx: AuthContext): Promise<Plan> {
300
+ return updatePlanNodeAsync(planId, nodeId, patch, ctx);
450
301
  }
451
302
 
452
303
  function optionalProp<K extends keyof PlanNode>(key: K, value: PlanNode[K] | undefined): Partial<PlanNode> {
@@ -470,72 +321,20 @@ function assertEdgeKeepsAcyclic(plan: Plan, edge: PlanEdge): void {
470
321
  validateEdges(projected.nodes, dedupeEdges([...projected.edges, edge]));
471
322
  }
472
323
 
473
- export function addPlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext): Plan {
474
- const plan = existingPlan(planId);
475
- const edge = cleanEdge(edgeInput);
476
- assertEdgeKeepsAcyclic(plan, edge);
477
- // #836: when BOTH endpoints are Task-bound, the canonical edge store is task_dependencies,
478
- // not the plan JSON. Plan edge {from,to} means `to` depends on `from`. The combined-graph
479
- // pre-check above already rejected cross-store cycles; the edge surfaces back via projection.
480
- const fromTask = boundTaskId(plan, edge.from);
481
- const toTask = boundTaskId(plan, edge.to);
482
- if (fromTask !== undefined && toTask !== undefined) {
483
- addTaskDependency(toTask, fromTask, actorId(ctx));
484
- const projected = projectPlanOverTasks(existingPlan(planId));
485
- emitPlan("plan.changed", projected, actorId(ctx));
486
- return projected;
487
- }
488
- // S6 #2079: fence behind the version-CAS; re-append + re-check acyclicity on the fresh snapshot.
489
- const updated = replacePlanNodeScoped(planId, (fresh) => {
490
- assertEdgeKeepsAcyclic(fresh, edge);
491
- return { ...fresh, edges: dedupeEdges([...fresh.edges, edge]) };
492
- }, actorId(ctx));
493
- emitPlan("plan.changed", updated, actorId(ctx)); return updated;
324
+ export async function addPlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext): Promise<Plan> {
325
+ return addPlanEdgeAsync(planId, edgeInput, ctx);
494
326
  }
495
327
 
496
- export function removePlanNode(planId: string, nodeId: string, ctx: AuthContext): Plan {
497
- const actor = actorId(ctx);
498
- // S6 #2079: fence the removal behind the version-CAS; re-resolve the node on the fresh snapshot.
499
- let removed!: PlanNode;
500
- const updated = replacePlanNodeScoped(planId, (plan) => {
501
- const current = plan.nodes.find((node) => node.id === nodeId);
502
- if (!current) throw new ValidationError(`node ${nodeId} not found`); removed = current;
503
- return {
504
- ...plan,
505
- nodes: plan.nodes.filter((node) => node.id !== nodeId),
506
- edges: plan.edges.filter((edge) => edge.from !== nodeId && edge.to !== nodeId),
507
- };
508
- }, actor);
509
- removePlanNodeIssueAssociation(updated.id, removed); emitPlan("plan.changed", updated, actor);
510
- return updated;
511
- }
512
-
513
- export function removePlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext): Plan {
514
- const plan = existingPlan(planId);
515
- const edge = cleanEdge(edgeInput);
516
- // A bound-pair edge can live in BOTH task_dependencies and the plan JSON, and projection unions
517
- // them: clear both stores or the edge reappears (#2877).
518
- const fromTask = boundTaskId(plan, edge.from);
519
- const toTask = boundTaskId(plan, edge.to);
520
- const depRemoved = fromTask !== undefined && toTask !== undefined && removeTaskDependency(toTask, fromTask, actorId(ctx));
521
- // S6 #2079: fence behind the version-CAS; re-resolve the edge (and its not-found check) on fresh.
522
- const updated = replacePlanNodeScoped(planId, (fresh) => {
523
- const edges = fresh.edges.filter((item) => item.from !== edge.from || item.to !== edge.to);
524
- if (!depRemoved && edges.length === fresh.edges.length) throw new ValidationError(`edge ${edge.from}->${edge.to} not found`);
525
- return { ...fresh, edges };
526
- }, actorId(ctx));
527
- const projected = projectPlanOverTasks(updated);
528
- emitPlan("plan.changed", projected, actorId(ctx));
529
- return projected;
530
- }
531
-
532
- export function deletePlanGraph(planId: string, ctx: AuthContext): { id: string; deleted: true } {
533
- const plan = existingPlan(planId);
534
- const actor = actorId(ctx);
535
- deletePlan(plan.id, actor);
536
- for (const node of plan.nodes) removePlanNodeIssueAssociation(plan.id, node);
537
- emitPlan("plan.deleted", plan, actor);
538
- return { id: plan.id, deleted: true };
328
+ export async function removePlanNode(planId: string, nodeId: string, ctx: AuthContext): Promise<Plan> {
329
+ return (await removePlanNodeAsync(planId, nodeId, ctx)).plan;
330
+ }
331
+
332
+ export async function removePlanEdge(planId: string, edgeInput: unknown, ctx: AuthContext): Promise<Plan> {
333
+ return removePlanEdgeAsync(planId, edgeInput, ctx);
334
+ }
335
+
336
+ export async function deletePlanGraph(planId: string, ctx: AuthContext): Promise<{ id: string; deleted: true }> {
337
+ return deletePlanGraphAsync(planId, ctx);
539
338
  }
540
339
 
541
340
  export async function archivePlanGraphAsync(planId: string, archived: boolean, ctx: AuthContext): Promise<Plan> {
@@ -547,29 +346,12 @@ export async function archivePlanGraphAsync(planId: string, archived: boolean, c
547
346
 
548
347
  // In-process (legacy/unit-test) sibling of the gateway-backed applyBoundOwnersAsync path (production
549
348
  // never takes this branch). S6 #2079: fence each write behind the version-CAS; re-derive on fresh.
550
- export function updateBoundPlanNodesForAgent(agentId: string, status: PlanStatus, actor = "server"): Plan[] {
551
- const boundFlip = (node: PlanNode): boolean =>
552
- node.statusSource === "bound" && node.ownerRef?.agentId === agentId && node.status !== "landed" && node.status !== status;
553
- const updated: Plan[] = [];
554
- for (const plan of listPlansWithBoundOwner(agentId)) {
555
- // Skip-no-op on the initial snapshot: don't even fence a write when there's nothing to flip.
556
- if (!plan.nodes.some(boundFlip)) continue;
557
- // Preserve that contract THROUGH a CAS-conflict retry: if a re-read snapshot no longer needs a
558
- // flip (a concurrent writer already applied it), return null so no version bump / event fires.
559
- let wrote = false;
560
- const next = replacePlanNodeScoped(plan.id, (fresh) => {
561
- wrote = fresh.nodes.some(boundFlip);
562
- return wrote ? { ...fresh, nodes: fresh.nodes.map((node) => (boundFlip(node) ? { ...node, status } : node)) } : null;
563
- }, actor);
564
- if (!wrote) continue; // the retry resolved to a no-op — nothing was written, so don't emit/collect
565
- emitPlan("plan.changed", next, actor); updated.push(next);
566
- }
567
- return updated;
349
+ export async function updateBoundPlanNodesForAgent(agentId: string, status: PlanStatus, actor = "server"): Promise<Plan[]> {
350
+ return updateBoundPlanNodesForAgentAsync(agentId, status, actor);
568
351
  }
569
352
 
570
- export function updateBoundPlanNodesFromAgent(agentId: string, actor = "server"): Plan[] {
571
- const status = boundStatusForAgent(getAgent(agentId));
572
- return status ? updateBoundPlanNodesForAgent(agentId, status, actor) : [];
353
+ export async function updateBoundPlanNodesFromAgent(agentId: string, actor = "server"): Promise<Plan[]> {
354
+ return updateBoundPlanNodesFromAgentAsync(agentId, boundStatusForAgent(getAgent(agentId)), actor);
573
355
  }
574
356
 
575
357
  /** Gateway-only live-binding path. The read precheck avoids submitting a no-op writer job. */
@@ -581,7 +363,7 @@ export async function updateBoundPlanNodesForAgentAsync(agentId: string, status:
581
363
  }
582
364
 
583
365
  export async function updateBoundPlanNodesFromAgentAsync(agentId: string, status: PlanStatus | undefined, actor = "server"): Promise<Plan[]> {
584
- return status ? updateBoundPlanNodesForAgentAsync(agentId, status, actor) : [];
366
+ return status ? (await updateBoundPlanNodesForAgentAsync(agentId, status, actor)) : [];
585
367
  }
586
368
 
587
369
  // #836: `view: "tasks"` returns the pure task-backed DAG synthesized from a team's deliverable
@@ -623,12 +405,8 @@ export interface BindPlanNodeInput {
623
405
  taskId?: number;
624
406
  }
625
407
 
626
- export function bindPlanNodeToTask(planId: string, nodeId: string, input: BindPlanNodeInput, ctx: AuthContext): { plan: Plan; taskId: number } {
627
- const actor = actorId(ctx);
628
- const result = writeTransaction(() => bindPlanNodeBody({ planId, nodeId, taskId: input.taskId, actor }, Date.now()));
629
- emitPlan("plan.changed", result.plan, actor);
630
- emitPlanTaskEvents(result.taskEvents);
631
- return { plan: result.plan, taskId: result.taskId! };
408
+ export async function bindPlanNodeToTask(planId: string, nodeId: string, input: BindPlanNodeInput, ctx: AuthContext): Promise<{ plan: Plan; taskId: number }> {
409
+ return bindPlanNodeToTaskAsync(planId, nodeId, input, ctx);
632
410
  }
633
411
 
634
412
  // HTTP and MCP can await. Keep the synchronous entry points above for the event reducers and
@@ -698,6 +476,6 @@ export async function bindPlanNodeToTaskAsync(planId: string, nodeId: string, in
698
476
  // The aggregate ack is after COMMIT. Emit every consumer-facing task intent only here,
699
477
  // never from the worker, so failed aggregate writes are observably silent.
700
478
  emitPlan("plan.changed", result.plan, actor);
701
- emitPlanTaskEvents(result.taskEvents);
479
+ await emitPlanTaskEvents(result.taskEvents);
702
480
  return { plan: result.plan, taskId: result.taskId! };
703
481
  }
@@ -60,7 +60,7 @@ export async function importPlan(input: Record<string, unknown>, ctx: AuthContex
60
60
  const { dryRun, ...args } = preparePlanImport(input, ctx);
61
61
  if (dryRun) return await getGateway().read("planGraph.import.preview", args) as unknown as PlanImportResult;
62
62
  const result = await getGateway().write("planGraph.import", args) as unknown as PlanImportWriteResult;
63
- emitPlanTaskEvents(result.taskEvents);
63
+ await emitPlanTaskEvents(result.taskEvents);
64
64
  if (result.plan) emitPlan(args.expectedVersion === 0 ? "plan.created" : "plan.changed", result.plan, args.actor);
65
65
  return result.receipt;
66
66
  }
@@ -1,8 +1,9 @@
1
- import { getTeam, getTeamAsync } from "../db";
2
- import { getGateway } from "../db/gateway";
1
+ import { updateBoundPlanNodesFromAgentAsync, updateBoundPlanNodesForAgentAsync } from "./plan-graph";
2
+ import { getTeamAsync } from "../db";
3
+ import { trackRelayEvent } from "../relay-event-drain";
3
4
  import { subscribeRelayEvents, type RelayEvent } from "../events";
4
5
  import { routeNotification } from "../notification-router";
5
- import { updateBoundPlanNodesForAgent, updateBoundPlanNodesForAgentAsync, updateBoundPlanNodesFromAgent, updateBoundPlanNodesFromAgentAsync } from "./plan-graph";
6
+
6
7
 
7
8
  let unsubscribe: (() => void) | undefined;
8
9
 
@@ -48,7 +49,7 @@ async function notifyBoundExit(event: RelayEvent, updatedPlans: Awaited<ReturnTy
48
49
  const target = team?.coordinatorId ?? `team:${plan.teamId}`;
49
50
  const blockedNodes = plan.nodes.filter((node) => node.statusSource === "bound" && node.ownerRef?.agentId === agentId && node.status === "blocked");
50
51
  if (blockedNodes.length === 0) continue;
51
- routeNotification({
52
+ await routeNotification({
52
53
  type: "plan.bound_agent_exited",
53
54
  scope: { agentId, planId: plan.id, teamId: plan.teamId },
54
55
  recipients: [target],
@@ -92,39 +93,9 @@ async function handlePlanBindingEventAsync(event: RelayEvent): Promise<void> {
92
93
  }
93
94
 
94
95
  function handlePlanBindingEvent(event: RelayEvent): void {
95
- // The in-process backend exists only for legacy/unit tests. Keep its old synchronous reducer
96
- // contract so these tests do not accidentally test scheduling; worker-backed production never
97
- // takes this branch and has no event-loop SQLite access.
98
- if (getGateway().stats.backend === "in-process") {
99
- if (event.type === "agent.status") {
100
- const agentId = agentIdFromEvent(event);
101
- if (agentId) updateBoundPlanNodesFromAgent(agentId, event.source || "server");
102
- return;
103
- }
104
- if (event.type === "branch.landed") {
105
- const agentId = stringField(event.data.author);
106
- if (agentId) updateBoundPlanNodesForAgent(agentId, "landed", event.source || "server");
107
- return;
108
- }
109
- if (event.type === "agent.exited") {
110
- const agentId = agentIdFromEvent(event);
111
- if (!agentId || isGracefulExitReason(stringField(event.data.reason))) return;
112
- const updated = updateBoundPlanNodesForAgent(agentId, "blocked", event.source || "server");
113
- const reason = stringField(event.data.reason);
114
- for (const plan of updated) {
115
- if (!plan.teamId) continue;
116
- const team = getTeam(plan.teamId);
117
- const nodes = plan.nodes.filter((node) => node.statusSource === "bound" && node.ownerRef?.agentId === agentId && node.status === "blocked");
118
- if (nodes.length) routeNotification({ type: "plan.bound_agent_exited", scope: { agentId, planId: plan.id, teamId: plan.teamId }, recipients: [team?.coordinatorId ?? `team:${plan.teamId}`], message: { subject: "Bound plan node blocked", body: `Agent \`${agentId}\` exited${reason ? ` (${reason})` : ""}; blocked plan node(s) ${nodes.map((node) => `\`${node.id}\``).join(", ")} in \`${plan.id}\`.`, payload: { kind: "plan.bound_agent_exited", planId: plan.id, agentId, nodeIds: nodes.map((node) => node.id), reason }, replyExpected: false } });
119
- }
120
- return;
121
- }
122
- }
123
- // Event listeners are synchronous. Serialize the asynchronous aggregate submissions so an
124
- // agent's rapid status sequence reaches the FIFO writer in event order.
125
- pending = pending.then(() => handlePlanBindingEventAsync(event)).catch((error) => {
126
- console.error("plan live binding update failed", error);
127
- });
96
+ const run = pending.then(() => handlePlanBindingEventAsync(event));
97
+ pending = run.catch(() => {});
98
+ trackRelayEvent(run, "plan.live-binding");
128
99
  }
129
100
 
130
101
  export function startPlanLiveBindings(): void {
@@ -72,7 +72,7 @@ export async function createPlanTaskAsync(input: CreateTaskEntityInput, extras:
72
72
  };
73
73
  // The caller's key makes a retry return the first result instead of a second Task or node.
74
74
  const result = await getGateway().write("planGraph.task.create", args, { idempotencyKey: options.idempotencyKey ?? randomUUID() }) as unknown as CreatePlanTaskResult;
75
- emitPlanTaskEvents(result.taskEvents);
75
+ await emitPlanTaskEvents(result.taskEvents);
76
76
  if (result.plan) emitPlan("plan.changed", result.plan, createdBy);
77
77
  // A replay returns the first write's result; the receipt reads the Task as it is now.
78
78
  const task = await getTaskDetailAsync(result.task.id) ?? result.task;