@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,3 +1,4 @@
1
+ import { isTokenUsageExpired } from "./token-usage-retention";
1
2
  // #2485 — the durable, provider-agnostic token-usage ledger ("the spine" of epic #2481). The three
2
3
  // capture seams write per-turn provider usage here through `tokenUsage.record`; the Analytics box
3
4
  // reads period/provider aggregates through `tokenUsage.aggregate`.
@@ -144,7 +145,7 @@ export function getSessionTokenUsageByDedupeKey(dedupeKey: string): SessionToken
144
145
  * the writer boundary as well as at the HTTP boundary, and never minted from the receipt clock,
145
146
  * so a genuine retry deterministically reuses the same key.
146
147
  */
147
- export function recordSessionTokenUsage(input: SessionTokenUsageInput): { record: SessionTokenUsageRecord; inserted: boolean } {
148
+ export function recordSessionTokenUsage(input: SessionTokenUsageInput): { record: SessionTokenUsageRecord | null; inserted: boolean } {
148
149
  const { createdAt } = input;
149
150
  if (!Number.isSafeInteger(createdAt) || createdAt <= 0) {
150
151
  throw new ValidationError("createdAt required (positive epoch-milliseconds of the provider's observed usage time)");
@@ -160,6 +161,8 @@ export function recordSessionTokenUsage(input: SessionTokenUsageInput): { record
160
161
  const prior = getDb().query("SELECT * FROM session_token_usage WHERE dedupe_key = ?").get(dedupeKey) as
161
162
  | SessionTokenUsageRow
162
163
  | undefined;
164
+ // Transcript capture replays from byte zero after restart; acknowledge folded history without recreating it (#3619).
165
+ if (!prior && isTokenUsageExpired(createdAt)) return { record: null, inserted: false };
163
166
  // Bound once, at first attribution: a replacement reuses the row's Task, never the agent's current one.
164
167
  const taskId = prior ? (prior.task_id ?? undefined) : deliverableTaskIdForAgent(input.agentId);
165
168
  getDb().query(`
@@ -0,0 +1,13 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getGateway } from "./gateway";
3
+ import type { createUserInvite, acceptUserInvite } from "./user-invites";
4
+ import { invalidateUserCount } from "./users";
5
+
6
+ export function createUserInviteAsync(input: Parameters<typeof createUserInvite>[0]): Promise<ReturnType<typeof createUserInvite>> {
7
+ return getGateway().write("userInvites.create", input, { idempotencyKey: randomUUID() }) as unknown as Promise<ReturnType<typeof createUserInvite>>;
8
+ }
9
+ export async function acceptUserInviteAsync(input: Parameters<typeof acceptUserInvite>[0]): Promise<ReturnType<typeof acceptUserInvite>> {
10
+ const user = await getGateway().write("userInvites.accept", input, { idempotencyKey: randomUUID() }) as unknown as ReturnType<typeof acceptUserInvite>;
11
+ invalidateUserCount();
12
+ return user;
13
+ }
@@ -4,7 +4,7 @@
4
4
  // the password identity, so a replayed or raced code can never create a second account.
5
5
  import { createHash, randomBytes } from "node:crypto";
6
6
  import { ValidationError, getDb, writeTransaction } from "./connection.ts";
7
- import { createUser } from "./users.ts";
7
+ import { createUserRow } from "./users.ts";
8
8
  import { createAuthIdentity } from "./auth-identities.ts";
9
9
  import type { User, UserRole } from "../types";
10
10
 
@@ -99,8 +99,8 @@ export function acceptUserInvite(input: { code: string; userId: string; handle:
99
99
  }
100
100
  const claimed = getDb().query("UPDATE user_invites SET used_at = ? WHERE id = ? AND used_at IS NULL").run(now, row.id);
101
101
  if (claimed.changes !== 1) throw new InviteUnavailableError("invite is invalid, expired or already used");
102
- const user = createUser({ id: input.userId, handle: input.handle, displayName: input.displayName || row.display_name || input.handle, role: row.role as UserRole });
103
- createAuthIdentity({ userId: user.id, provider: input.provider, providerSubject: input.handle, secretHash: input.secretHash });
102
+ const user = createUserRow({ id: input.userId, handle: input.handle, displayName: input.displayName || row.display_name || input.handle, role: row.role as UserRole }, now);
103
+ createAuthIdentity({ userId: user.id, provider: input.provider, providerSubject: input.handle, secretHash: input.secretHash }, now);
104
104
  getDb().query("UPDATE user_invites SET used_by = ? WHERE id = ?").run(user.id, row.id);
105
105
  return user;
106
106
  });
package/src/db/users.ts CHANGED
@@ -91,10 +91,19 @@ export function userCount(now: number = Date.now()): number {
91
91
  }
92
92
 
93
93
  export function createUser(input: CreateUserInput): User {
94
+ const user = createUserRow(input);
95
+ invalidateUserCount();
96
+ return user;
97
+ }
98
+
99
+ export function invalidateUserCount(): void {
94
100
  assertMainThreadState("users.ts");
101
+ userCountCache = undefined;
102
+ }
103
+
104
+ export function createUserRow(input: CreateUserInput, now = Date.now()): User {
95
105
  if (!input.id) throw new ValidationError("user id is required");
96
106
  if (!input.handle) throw new ValidationError("user handle is required");
97
- const now = Date.now();
98
107
  getDb()
99
108
  .query(
100
109
  `INSERT INTO users (id, handle, display_name, role, status, tenant_id, avatar_artifact_id, created_at, updated_at)
@@ -111,7 +120,6 @@ export function createUser(input: CreateUserInput): User {
111
120
  now,
112
121
  now,
113
122
  );
114
- userCountCache = undefined;
115
123
  const created = getUser(input.id);
116
124
  if (!created) throw new ValidationError(`failed to create user ${input.id}`);
117
125
  return created;
@@ -121,7 +129,7 @@ export function updateUserProfile(input: {
121
129
  id: string;
122
130
  displayName?: string;
123
131
  avatarArtifactId?: string | null;
124
- }): User {
132
+ }, submittedAt: number = Date.now()): User {
125
133
  const current = getUser(input.id);
126
134
  if (!current) throw new ValidationError("user not found");
127
135
  const updates: string[] = [];
@@ -137,17 +145,17 @@ export function updateUserProfile(input: {
137
145
  }
138
146
  if (!updates.length) return current;
139
147
  updates.push("updated_at = ?");
140
- params.push(Date.now(), input.id);
148
+ params.push(submittedAt, input.id);
141
149
  getDb().query(`UPDATE users SET ${updates.join(", ")} WHERE id = ?`).run(...params);
142
150
  return getUser(input.id)!;
143
151
  }
144
152
 
145
153
  /** Change a user's role or account status (#3567 admin user list). */
146
- export function updateUserAccess(input: { id: string; role?: UserRole; status?: UserStatus }): User {
154
+ export function updateUserAccess(input: { id: string; role?: UserRole; status?: UserStatus }, submittedAt: number = Date.now()): User {
147
155
  const current = getUser(input.id);
148
156
  if (!current) throw new ValidationError("user not found");
149
157
  getDb().query("UPDATE users SET role = ?, status = ?, updated_at = ? WHERE id = ?")
150
- .run(input.role ?? current.role, input.status ?? current.status, Date.now(), input.id);
158
+ .run(input.role ?? current.role, input.status ?? current.status, submittedAt, input.id);
151
159
  return getUser(input.id)!;
152
160
  }
153
161
 
@@ -1,8 +1,8 @@
1
- // Task ownership and coordinator routing see the same execution facts for runs and item lanes.
1
+ // Task ownership, coordinator routing and spawn admission share execution facts for runs and item lanes.
2
2
  export const WORKFLOW_EXECUTIONS_SQL = `(
3
- SELECT scope, id, taskId, coordinator, status, reason, awaiting FROM kernel_entity__workflow__run
3
+ SELECT scope, id, taskId, coordinator, callerAgentId, status, reason, awaiting FROM kernel_entity__workflow__run
4
4
  UNION ALL
5
- SELECT item.scope, item.id, owner.taskId, owner.coordinator,
5
+ SELECT item.scope, item.id, owner.taskId, owner.coordinator, owner.callerAgentId,
6
6
  json_extract(item.lane, '$.status') AS status, json_extract(item.lane, '$.reason') AS reason, json_extract(item.lane, '$.awaiting') AS awaiting
7
7
  FROM kernel_entity__workflow__activation item
8
8
  JOIN kernel_entity__workflow__run owner ON owner.scope = item.scope AND owner.id = item.runId
@@ -1,3 +1,4 @@
1
+ import { trackRelayEvent } from "../relay-event-drain";
1
2
  import { mainThreadRegistry } from "./main-thread-state";
2
3
  import { persistWorkspace, type WorkspaceUpsertInput } from "./workspace-persistence";
3
4
  import { Database } from "bun:sqlite";
@@ -438,7 +439,7 @@ export function pruneOrphanedSharedWorkspaces(): string[] {
438
439
  // owner's branch-state badge" (#236). A hook, not a direct emit, because getDb().ts is
439
440
  // the lowest layer — sse.ts imports db, so db can't import sse without a cycle. sse
440
441
  // registers a listener at startup that re-emits the owner agent's status over SSE.
441
- type WorkspaceChangeListener = (workspace: WorkspaceRecord) => void;
442
+ type WorkspaceChangeListener = (workspace: WorkspaceRecord) => unknown;
442
443
  export const workspaceChangeListeners = mainThreadRegistry("workspaces.ts:workspaceChangeListeners", new Set<WorkspaceChangeListener>());
443
444
  export function onWorkspaceChange(listener: WorkspaceChangeListener): void {
444
445
  workspaceChangeListeners.add(listener);
@@ -447,7 +448,8 @@ export function emitWorkspaceChange(workspace: WorkspaceRecord | null | undefine
447
448
  if (!workspace) return;
448
449
  for (const listener of workspaceChangeListeners) {
449
450
  try {
450
- listener(workspace);
451
+ const result = listener(workspace);
452
+ if (result instanceof Promise) trackRelayEvent(result, "workspace.changed");
451
453
  } catch {
452
454
  // A badge-refresh listener must never break a workspace write.
453
455
  }
@@ -455,15 +457,16 @@ export function emitWorkspaceChange(workspace: WorkspaceRecord | null | undefine
455
457
  }
456
458
 
457
459
  // #1870 — workspace-create effects stay on main; index.ts registers the issue activity handler.
458
- type WorkspaceCreateListener = (workspace: WorkspaceRecord) => void;
460
+ type WorkspaceCreateListener = (workspace: WorkspaceRecord) => void | Promise<void>;
459
461
  const workspaceCreateListeners = mainThreadRegistry("workspaces.ts:workspaceCreateListeners", new Set<WorkspaceCreateListener>());
460
462
  export function onWorkspaceCreate(listener: WorkspaceCreateListener): void {
461
463
  workspaceCreateListeners.add(listener);
462
464
  }
463
- function emitWorkspaceCreate(workspace: WorkspaceRecord): void {
465
+ export function emitWorkspaceCreate(workspace: WorkspaceRecord): void {
464
466
  for (const listener of workspaceCreateListeners) {
465
467
  try {
466
- listener(workspace);
468
+ const result = listener(workspace);
469
+ if (result) trackRelayEvent(result, "workspace.created");
467
470
  } catch {
468
471
  // A create listener must never break a workspace write.
469
472
  }
@@ -1,3 +1,10 @@
1
+ import { TIMER_CUTOVER_COORDINATOR_WRITE_OPS } from "./timer-cutover-bodies";
2
+ import { RUNTIME_ADMIN_WRITE_OPS } from "./runtime-admin-write-ops";
3
+ import { AUTOMATION_WRITE_OPS } from "./automation-write-ops";
4
+ import { RUNTIME_TAIL_WRITE_OPS } from "./runtime-tail-write-ops";
5
+ import { LIFECYCLE_TAIL_WRITE_OPS } from "./lifecycle-tail-write-ops";
6
+ import { NOTIFICATION_WRITE_OPS } from "./notification-write-ops";
7
+ import { HISTORY_RETENTION_WRITE_OPS } from "./history-retention-ops";
1
8
  import { INTEGRATIONS_WRITE_OPS } from "./integrations-ops.ts";
2
9
  import { bufferedLastSeen, type PresenceSnapshot } from "./presence-buffer";
3
10
  import { ensureDurablePayloadSchema, readRetryReceipt, storeDurablePayload } from "./durable-payloads";
@@ -42,6 +49,7 @@ import { CLAUDE_API_BUDGET_WRITE_OPS } from "./claude-api-budget-ops";
42
49
  // file is the seam they plug into, not the final op set.
43
50
 
44
51
  import type { Database } from "bun:sqlite";
52
+ import { WRITER_FAULT_TEST_OPS } from "./writer-fault-test-ops";
45
53
  import { ValidationError } from "@humwick/sdk";
46
54
  import { getDb, inWriteTransaction } from "./connection.ts";
47
55
  import type { SerializedDbError } from "./db-error-registry.ts";
@@ -93,6 +101,7 @@ import { PLANS_WRITE_OPS } from "./plans-write-ops.ts";
93
101
  import { CONTINUATION_ARCHIVES_WRITE_OPS } from "./continuation-archives-write-ops.ts";
94
102
  import { WORKFLOW_JOURNAL_WRITE_OPS } from "./workflow-journal-write-ops.ts";
95
103
  import { WORKFLOW_STEP_TASK_WRITE_OPS } from "./workflow-step-tasks.ts";
104
+ import { PROJECT_ENTRIES_WRITE_OPS } from "./project-entries-write-ops";
96
105
  import { PROJECTS_WRITE_OPS } from "./projects-write-ops.ts";
97
106
  import { CHANNELS_WRITE_OPS } from "./channels-write-ops.ts";
98
107
  import { CI_INCIDENTS_WRITE_OPS } from "./ci-incidents-write-ops.ts";
@@ -199,6 +208,10 @@ export interface WriterOpSpec {
199
208
  /** This complete operation has no main-thread effects and may use the file-backed writer. */
200
209
  readonly offLoop?: boolean;
201
210
  readonly idempotent: boolean;
211
+ /** #3614 — a non-idempotent op whose keyless callers never resubmit: it runs without a key and
212
+ * writes no ledger row, and a lost acknowledgement stays indeterminate for that caller. A caller
213
+ * that does retry still passes a key and gets the ledger's exactly-once dedup. */
214
+ readonly atMostOnce?: true;
202
215
  readonly fn: WriterOp;
203
216
  /** Sanitizes a result before it enters the durable retry ledger. Results can include a newly-created
204
217
  * command's caller-visible plaintext env, while the ledger is readable as raw DB state. */
@@ -234,14 +247,7 @@ const PROBE_DDL = `
234
247
  // durable across a worker death (the phantom-failure case), because it lives in the DB, not in memory.
235
248
  export { WRITER_IDEMPOTENCY_RETENTION_MS };
236
249
 
237
- /**
238
- * Create the seam-probe + dedup schema. This models real schema bootstrap (§4, the allowlisted
239
- * bootstrap layer): DDL runs ONCE at startup as its own committed statement, NOT lazily inside an op —
240
- * a rolled back op must never undo the schema it ran against. Ops below therefore assume the tables
241
- * exist, just as relocated real ops assume the migrated catalog exists (H6/§3.1). Idempotent
242
- * (`IF NOT EXISTS`), so it is safe to call on both the in-process connection and, at bootstrap, on the
243
- * file that the worker later opens (WAL makes the committed tables visible cross-connection, §3.5).
244
- */
250
+ // Probe tables are installed at bootstrap, outside writer transactions.
245
251
  export function ensureSeamProbeSchema(db: Database = getDb()): void {
246
252
  if (db.inTransaction) throw new Error("ensureSeamProbeSchema must run at bootstrap, not inside a transaction");
247
253
  db.run(PROBE_DDL);
@@ -277,6 +283,7 @@ export const WRITER_OPS: Readonly<Record<string, WriterOpSpec>> = Object.freeze(
277
283
  // call inserts a NEW row — NON-idempotent: a retry without dedup would double-apply, so the gateway
278
284
  // requires an idempotencyKey and the executor dedups on it.
279
285
  "seam.append": {
286
+ offLoop: true,
280
287
  idempotent: false,
281
288
  fn: (args: { tag: string; value?: string }, ctx) => {
282
289
  const row = getDb()
@@ -396,11 +403,17 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
396
403
  if (!mergedWriterOps) {
397
404
  mergedWriterOps = Object.freeze({
398
405
  ...WRITER_OPS,
406
+ ...WRITER_FAULT_TEST_OPS,
399
407
  ...VAULT_TOKEN_WRITE_OPS,
400
408
  ...VAULT_HEALTH_WRITE_OPS,
401
409
  ...WORKSPACE_REKEY_WRITE_OPS,
402
410
  ...PAIRS_WRITE_OPS,
403
411
  ...MESSAGE_WRITE_OPS,
412
+ ...NOTIFICATION_WRITE_OPS,
413
+ ...LIFECYCLE_TAIL_WRITE_OPS,
414
+ ...RUNTIME_TAIL_WRITE_OPS,
415
+ ...AUTOMATION_WRITE_OPS,
416
+ ...RUNTIME_ADMIN_WRITE_OPS,
404
417
  ...COMMANDS_WRITE_OPS,
405
418
  ...RESTART_EPOCH_WRITE_OPS,
406
419
  ...BUS_OUTBOX_WRITE_OPS,
@@ -416,6 +429,7 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
416
429
  ...TIMER_INGRESS_WRITE_OPS,
417
430
  ...TIMER_TRANSFER_WRITE_OPS,
418
431
  ...TIMER_CUTOVER_WRITE_OPS,
432
+ ...TIMER_CUTOVER_COORDINATOR_WRITE_OPS,
419
433
  ...TASK_WRITE_OPS,
420
434
  ...AGENTS_WRITE_OPS,
421
435
  ...AGENT_LIFECYCLE_WRITE_OPS,
@@ -443,6 +457,7 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
443
457
  ...WORKFLOW_STEP_TASK_WRITE_OPS,
444
458
  ...SPAWN_TREE_CLEANUP_WRITE_OPS,
445
459
  ...PROJECTS_WRITE_OPS,
460
+ ...PROJECT_ENTRIES_WRITE_OPS,
446
461
  ...CHANNELS_WRITE_OPS,
447
462
  ...INTEGRATIONS_WRITE_OPS,
448
463
  ...CI_INCIDENTS_WRITE_OPS,
@@ -476,6 +491,7 @@ export function allWriterOps(): Readonly<Record<string, WriterOpSpec>> {
476
491
  ...ISSUE_REFS_WRITE_OPS,
477
492
  ...WORKSPACE_WRITE_OPS,
478
493
  ...WEB_TOOL_CALL_WRITE_OPS,
494
+ ...HISTORY_RETENTION_WRITE_OPS,
479
495
  ...CLAUDE_API_BUDGET_WRITE_OPS,
480
496
  ...CLAUDE_APP_OAUTH_WRITE_OPS,
481
497
  });
@@ -487,10 +503,12 @@ export function isKnownWriterOp(op: string): boolean {
487
503
  return Object.prototype.hasOwnProperty.call(allWriterOps(), op);
488
504
  }
489
505
 
490
- /** Whether a registered op is safe to re-apply without an idempotency key (H5 §3.7.1). Throws for an
491
- * unknown op so a typo cannot silently pass the gateway's keyless-write gate. */
492
- export function isIdempotentWriterOp(op: string): boolean {
493
- return resolveOp(op).idempotent;
506
+ /** Whether a registered op may be submitted without an idempotency key (H5 §3.7.1): it is safe to
507
+ * re-apply, or it is at-most-once. Throws for an unknown op so a typo cannot silently pass the
508
+ * gateway's keyless-write gate. */
509
+ export function writerOpAllowsKeyless(op: string): boolean {
510
+ const spec = resolveOp(op);
511
+ return spec.idempotent || spec.atMostOnce === true;
494
512
  }
495
513
 
496
514
  export function isOffLoopWriterOp(op: string): boolean {
@@ -512,6 +530,7 @@ function resolveOp(op: string): WriterOpSpec {
512
530
  */
513
531
  function applyOnce(op: string, spec: WriterOpSpec, args: unknown, ctx: WriterOpContext, key: string | undefined): unknown {
514
532
  if (spec.idempotent) return spec.fn(args, ctx); // safe to re-apply, no key needed
533
+ if (key === undefined && spec.atMostOnce) return spec.fn(args, ctx); // no retry, so nothing to dedup
515
534
  // STRUCTURAL enforcement at the executor (H5 §3.7.1): a non-idempotent op MUST carry a key here, so
516
535
  // the invariant holds even for a direct executor caller that bypassed the gateway guard. Rejecting
517
536
  // BEFORE spec.fn means zero rows are written, so a keyless non-idempotent write can never duplicate.
@@ -24,10 +24,6 @@
24
24
  // `threadId` is still recorded and checked as belt-and-braces for an exotic case where a fault is
25
25
  // armed on a thread that later dispatches ops elsewhere.
26
26
  //
27
- // This is deliverable under the IN-PROCESS backend, which is the gateway default (`initGateway`
28
- // chooses in-process even for a file DB) and therefore what the whole default suite and every
29
- // existing chaos-worker subprocess run — so no existing crash seam loses its injection.
30
-
31
27
  import { threadId } from "node:worker_threads";
32
28
 
33
29
  interface ArmedWriterFault {
@@ -36,10 +32,7 @@ interface ArmedWriterFault {
36
32
  readonly threadId: number;
37
33
  }
38
34
 
39
- // Deliberately NOT a mainThreadRegistry (#1870 S6a): off-loop op bodies call `writerFault`, and on the
40
- // writer thread this map is empty, so the lookup is a harmless no-op there. Delivery is kept honest at
41
- // dispatch: while a fault is armed the in-process backend runs off-loop ops on the arming thread
42
- // (writer-gateway `dispatchEntry`), and arming refuses under the worker backend.
35
+ // Each writer thread owns its fault registry. File-backed tests arm named actions on that thread.
43
36
  const armed = new Map<string, ArmedWriterFault>();
44
37
 
45
38
  /**
@@ -67,7 +60,7 @@ export function armWriterFault(name: string, fn: () => void): () => void {
67
60
  throw new Error(
68
61
  `writer fault '${name}' cannot be armed: the gateway writer backend is 'worker', so this ` +
69
62
  `main-thread closure can never run inside the writer op's transaction and the crash would ` +
70
- `not be injected at all. Arm faults against the in-process backend (the gateway default), ` +
63
+ `not be injected at all. Use :memory: for closure faults, arm a named seam.armFault action on the writer, ` +
71
64
  `or assert the behaviour without an in-transaction crash seam.`,
72
65
  );
73
66
  }
@@ -0,0 +1,18 @@
1
+ import { armWriterFault } from "./writer-fault-seam";
2
+ import type { WriterOpSpec } from "./write-ops";
3
+
4
+ // Named actions cross the worker seam; main-thread closures cannot (#1870 S8).
5
+ export const WRITER_FAULT_TEST_OPS = {
6
+ "seam.armFault": {
7
+ offLoop: true,
8
+ idempotent: true,
9
+ fn: (args: { name: string; action: "kill" | "throw" }) => {
10
+ if (args.action !== "kill" && args.action !== "throw") throw new Error("unknown writer test fault");
11
+ armWriterFault(args.name, () => {
12
+ if (args.action === "kill") process.kill(process.pid, "SIGKILL");
13
+ throw new Error(`injected writer fault: ${args.name}`);
14
+ });
15
+ return null;
16
+ },
17
+ },
18
+ } satisfies Record<string, WriterOpSpec>;
@@ -42,17 +42,16 @@ import { WriterDrain } from "./writer-drain";
42
42
 
43
43
  import { errMessage } from "@humwick/sdk";
44
44
  import { createWorkerPool, type WorkerPool } from "@humwick/offload";
45
- import { getDbGeneration, getDbPath } from "./connection.ts";
46
- import { armedWriterFaultCount } from "./writer-fault-seam.ts";
47
- import { onLoopWriteTransaction } from "./on-loop-write-transaction.ts";
45
+ import { getDbPath } from "./connection.ts";
48
46
  // On-loop-block attribution (#1882 / ADR §5). `resolveLoopCallsite` pins the effective callsite onto a
49
47
  // submission in the CALLER's context; `captureSyncFrame` times the op body where it actually runs — in
50
48
  // the in-process drain microtask, NOT the synchronous enqueue. One-way import (loop-block-attribution
51
49
  // imports only event-loop-lag + config), so no cycle.
52
50
  import { captureSyncFrame, resolveLoopCallsite } from "../loop-block-attribution.ts";
51
+ import { markCallerResume } from "../hub-metrics.ts";
53
52
  import { reconstructDbError, serializeDbError } from "./db-error-registry.ts";
54
53
  import {
55
- isIdempotentWriterOp,
54
+ writerOpAllowsKeyless,
56
55
  isOffLoopWriterOp,
57
56
  runWriterBatch,
58
57
  runWriterOp,
@@ -110,7 +109,7 @@ export class WriteIndeterminateError extends Error {
110
109
  // ── Options & per-call opts ───────────────────────────────────────────────────────────────────────
111
110
 
112
111
  export interface InitWriterOptions {
113
- /** Overrides the path from `getDbPath()`; `:memory:`/undefined selects the in-process backend. */
112
+ /** File path to attach. A live file DB always uses its worker, including legacy :memory: requests. */
114
113
  path?: string;
115
114
  /** Soft high-water mark on the writer queue; past it, non-critical writes shed (§3.6). */
116
115
  queueHighWater?: number;
@@ -183,6 +182,9 @@ export interface WriterStats {
183
182
  * the observable signal that write convoys are long enough to need the throttle.
184
183
  */
185
184
  drainYields: number;
185
+ /** #3798 — writes executed since boot on the main thread and on a writer thread (a batch counts its entries). */
186
+ onLoopOps: number;
187
+ writerThreadOps: number;
186
188
  }
187
189
 
188
190
  // ── Backend state ─────────────────────────────────────────────────────────────────────────────────
@@ -243,8 +245,6 @@ interface InProcessBackend {
243
245
  kind: "in-process";
244
246
  stop: AbortController;
245
247
  busy: boolean;
246
- offLoop?: WorkerBackend;
247
- offLoopGeneration?: number;
248
248
  }
249
249
  type Backend = WorkerBackend | InProcessBackend;
250
250
 
@@ -256,6 +256,8 @@ let writerTimeoutMs = DEFAULT_WRITER_TIMEOUT_MS;
256
256
  let drainSliceMs = DEFAULT_DRAIN_SLICE_MS;
257
257
  // Monotonic across re-inits (like any counter): a test that wants a delta reads it before and after.
258
258
  let drainYields = 0;
259
+ let onLoopOps = 0;
260
+ let writerThreadOps = 0;
259
261
  // Set while a writer job has blown its gateway deadline and the sole writer is stalled holding the
260
262
  // lock (§3.6.1). Cleared when that in-flight job finally settles (the op completed and released the
261
263
  // lock), which re-kicks the FIFO drain. Worker backend only — an in-process op runs synchronously on
@@ -265,17 +267,17 @@ let wedgedOp: { op: string; startedAt: number } | undefined;
265
267
  // Test-only fault injection for the precise H5 window: the op has COMMITTED, but its
266
268
  // acknowledgement is lost before the caller observes the result. One-shot by design so a retry
267
269
  // exercises the persisted idempotency ledger rather than repeatedly failing at the transport edge.
268
- let postCommitAckLossForTests: { op: string; remaining: number } | undefined;
270
+ let postCommitAckLossForTests: { op: string } | undefined;
269
271
 
270
272
  export function rejectNextWriterAckAfterCommitForTests(op: string): void {
271
- postCommitAckLossForTests = { op, remaining: 1 };
273
+ postCommitAckLossForTests = { op };
272
274
  }
273
275
 
274
- function losePostCommitAckForTests(op: string): boolean {
275
- if (!postCommitAckLossForTests || postCommitAckLossForTests.op !== op) return false;
276
- postCommitAckLossForTests.remaining -= 1;
277
- if (postCommitAckLossForTests.remaining <= 0) postCommitAckLossForTests = undefined;
278
- return true;
276
+ function resolveWriterOp(entry: Extract<Submission, { kind: "op" }>, result: unknown): void {
277
+ if (postCommitAckLossForTests?.op === entry.op) {
278
+ postCommitAckLossForTests = undefined;
279
+ entry.reject(new WriteIndeterminateError(entry.op, new Error("injected post-commit lost acknowledgement"), entry.opts?.idempotencyKey));
280
+ } else entry.resolve(result);
279
281
  }
280
282
 
281
283
  /**
@@ -284,7 +286,8 @@ function losePostCommitAckForTests(op: string): boolean {
284
286
  * size-1 writer pool.
285
287
  */
286
288
  export function initWriter(options: InitWriterOptions = {}): void {
287
- const path = options.path ?? getDbPath();
289
+ // A file-backed main handle never executes writer bodies, even when an old caller requests memory.
290
+ const path = options.path && options.path !== ":memory:" ? options.path : getDbPath() ?? options.path;
288
291
  // EVERY caller-supplied number is validated BEFORE any of them is assigned: a RangeError thrown
289
292
  // after an earlier field was already written would leave the module in a half-applied config the
290
293
  // caller never asked for.
@@ -371,7 +374,6 @@ export async function shutdownWriter(): Promise<void> {
371
374
  }
372
375
  if (current?.kind === "in-process") current.stop.abort(new Error("writer gateway shut down"));
373
376
  if (current?.kind === "worker") await current.pool.destroy();
374
- if (current?.kind === "in-process") await current.offLoop?.pool.destroy();
375
377
  }
376
378
 
377
379
  /** Whether the gateway is initialized (some callers guard bootstrap ordering on this). */
@@ -380,13 +382,14 @@ export function isWriterInitialized(): boolean {
380
382
  }
381
383
 
382
384
  export function writerStats(): WriterStats {
383
- if (!backend) return { backend: "uninitialized", queued: 0, busy: 0, highWater: queueHighWater, wedged: null, drainYields };
385
+ const ops = { drainYields, onLoopOps, writerThreadOps };
386
+ if (!backend) return { backend: "uninitialized", queued: 0, busy: 0, highWater: queueHighWater, wedged: null, ...ops };
384
387
  const wedged = wedgedOp ? { op: wedgedOp.op, runningMs: Math.round(performance.now() - wedgedOp.startedAt) } : null;
385
388
  if (backend.kind === "in-process") {
386
- return { backend: "in-process", queued: pendingWriteDepth(), busy: inFlightWrites(), highWater: queueHighWater, wedged, drainYields };
389
+ return { backend: "in-process", queued: pendingWriteDepth(), busy: inFlightWrites(), highWater: queueHighWater, wedged, ...ops };
387
390
  }
388
391
  const stats = backend.pool.stats;
389
- return { backend: "worker", queued: pendingWriteDepth(), busy: stats.busy, highWater: queueHighWater, wedged, drainYields };
392
+ return { backend: "worker", queued: pendingWriteDepth(), busy: stats.busy, highWater: queueHighWater, wedged, ...ops };
390
393
  }
391
394
 
392
395
  function requireBackend(): Backend {
@@ -450,7 +453,7 @@ function pendingWriteDepth(): number {
450
453
 
451
454
  /** Includes an in-process writer waiting asynchronously for BEGIN. */
452
455
  function inFlightWrites(): number {
453
- return backend?.kind === "worker" ? backend.pool.stats.busy : Math.max(Number(backend?.busy ?? false), backend?.offLoop?.pool.stats.busy ?? 0);
456
+ return backend?.kind === "worker" ? backend.pool.stats.busy : Number(backend?.busy ?? false);
454
457
  }
455
458
 
456
459
  /** Reject every caller attached to a submission (used for teardown / dispatch-time backend loss). */
@@ -463,7 +466,7 @@ function rejectEntry(entry: Submission, error: unknown): void {
463
466
  * before the write is ever submitted, so a keyless non-idempotent write can never be retried into a
464
467
  * duplicate. `key` is the per-write key (the batch/coalesce paths pass each entry's own key). */
465
468
  function guardIdempotency(op: string, key: string | undefined): void {
466
- if (key === undefined && !isIdempotentWriterOp(op)) throw new WriteRetrySafetyError(op);
469
+ if (key === undefined && !writerOpAllowsKeyless(op)) throw new WriteRetrySafetyError(op);
467
470
  }
468
471
 
469
472
  /** Backpressure admission (§3.6/§3.6.1). `count` is the number of writes this submission would ADD (1
@@ -553,19 +556,9 @@ async function dispatchEntry(entry: Submission): Promise<void> {
553
556
  if (back.kind === "in-process") {
554
557
  back.busy = true;
555
558
  try {
556
- const path = getDbPath();
557
- // An armed crash seam is a closure on THIS thread, so while one is armed the op runs here instead
558
- // of on the file-DB worker (tests only; production never arms). Otherwise the fault would never fire.
559
- if (path && path !== ":memory:" && isOffLoopWriterOp(entry.op) && armedWriterFaultCount() === 0) {
560
- if (back.offLoop && back.offLoopGeneration !== getDbGeneration()) {
561
- await back.offLoop.pool.destroy();
562
- back.offLoop = undefined;
563
- }
564
- back.stop.signal.throwIfAborted();
565
- back.offLoop ??= createFileWriter(path);
566
- back.offLoopGeneration = getDbGeneration();
567
- await dispatchWorker(entry, back.offLoop);
568
- } else await dispatchInProcess(entry, back.stop.signal);
559
+ back.stop.signal.throwIfAborted();
560
+ onLoopOps += entryWrites(entry);
561
+ await dispatchInProcess(entry);
569
562
  } catch (error) { rejectEntry(entry, error); }
570
563
  finally { back.busy = false; }
571
564
  return;
@@ -573,6 +566,10 @@ async function dispatchEntry(entry: Submission): Promise<void> {
573
566
  await dispatchWorker(entry, back);
574
567
  }
575
568
 
569
+ function entryWrites(entry: Submission): number {
570
+ return entry.kind === "op" ? 1 : entry.argsList.length;
571
+ }
572
+
576
573
  /**
577
574
  * The per-entry dedup keys for a batch/coalesce job (H5 §3.7.1). A dbWriteBatch carries ONE key for
578
575
  * the whole call and derives a stable per-entry key `${key}#${i}`, so a blind retry of the same batch
@@ -600,18 +597,12 @@ function entryKeys(entry: Extract<Submission, { kind: "batch" | "coalesce" }>):
600
597
  * (`dispatchWorker`) is deliberately NOT wrapped: its op body runs off-thread, so there is no on-loop
601
598
  * block to attribute (only the sub-floor enqueue, which the record floor drops).
602
599
  */
603
- async function dispatchInProcess(entry: Submission, signal: AbortSignal): Promise<void> {
604
- const execute = <T>(fn: () => T) => onLoopWriteTransaction(
605
- fn, (attempt) => captureSyncFrame({ op: entry.op, callsite: entry.callsite }, attempt), writerTimeoutMs, signal,
606
- );
600
+ async function dispatchInProcess(entry: Submission): Promise<void> {
601
+ const execute = <T>(fn: () => T) => captureSyncFrame({ op: entry.op, callsite: entry.callsite }, fn);
607
602
  if (entry.kind === "op") {
608
603
  try {
609
- const result = cloneIn(await execute(() => runWriterOp(entry.op, cloneIn(entry.args), { now: entry.now, presence: cloneIn(entry.presence) }, entry.opts?.idempotencyKey)));
610
- if (losePostCommitAckForTests(entry.op)) {
611
- entry.reject(new WriteIndeterminateError(entry.op, new Error("injected post-commit lost acknowledgement"), entry.opts?.idempotencyKey));
612
- return;
613
- }
614
- entry.resolve(result);
604
+ const result = cloneIn(execute(() => runWriterOp(entry.op, cloneIn(entry.args), { now: entry.now, presence: cloneIn(entry.presence) }, entry.opts?.idempotencyKey)));
605
+ resolveWriterOp(entry, result);
615
606
  } catch (error) {
616
607
  entry.reject(reconstructInProcess(error));
617
608
  }
@@ -620,7 +611,7 @@ async function dispatchInProcess(entry: Submission, signal: AbortSignal): Promis
620
611
  // batch + coalesce share the SAVEPOINT-per-caller executor; only the fan-out differs.
621
612
  let coalesced: CoalescedEntry[];
622
613
  try {
623
- const entries: BatchEntry[] = await execute(() => runWriterBatch(
614
+ const entries: BatchEntry[] = execute(() => runWriterBatch(
624
615
  entry.op,
625
616
  entry.argsList.map((a) => cloneIn(a)),
626
617
  entryKeys(entry),
@@ -653,6 +644,10 @@ async function dispatchWorker(entry: Submission, back: WorkerBackend): Promise<v
653
644
  // retrySafe stays false (default): a write must NEVER be auto-re-dispatched (it would repeat its
654
645
  // effect). No pool timeout is armed — the deadline is the gateway's own (§3.6.1).
655
646
  const startedAt = performance.now();
647
+ // #3798 — the drain runs outside any caller, so its dispatch and result handling are charged to the
648
+ // caller that submitted this entry (the reader pool does the same per read).
649
+ markCallerResume(entry.callsite);
650
+ writerThreadOps += entryWrites(entry);
656
651
  const poolPromise = back.pool.run<WriterJobInput, WriterJobResult>(WRITER_TASK_NAME, input, { labels });
657
652
 
658
653
  let timer: ReturnType<typeof setTimeout> | undefined;
@@ -663,6 +658,7 @@ async function dispatchWorker(entry: Submission, back: WorkerBackend): Promise<v
663
658
  }),
664
659
  ]);
665
660
  if (timer) clearTimeout(timer);
661
+ markCallerResume(entry.callsite);
666
662
 
667
663
  if (raced.done === "timeout") {
668
664
  // WEDGED (§3.6.1). The op is still running and still holds the lock; do NOT replace the worker.
@@ -707,7 +703,7 @@ function fanOutWorker(entry: Submission, result: WriterJobResult): void {
707
703
  entry.reject(new Error(`writer worker returned a ${result.kind} result for a single op`));
708
704
  return;
709
705
  }
710
- if (result.ok) entry.resolve(result.result);
706
+ if (result.ok) resolveWriterOp(entry, result.result);
711
707
  else entry.reject(reconstructDbError(result.error));
712
708
  return;
713
709
  }