@sema-agent/server 7.43.0 → 7.44.0

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 (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,125 +1,24 @@
1
- /**
2
- * P1 ①② — the async-workflow COMPLETION INBOX (the deployment half of "route a finished
3
- * background workflow back to the session that started it").
4
- *
5
- * ## Why this exists
6
- * A self-orchestration / background workflow (started via core's `RunWorkflow`) runs to terminal OUTSIDE the
7
- * originating session's request loop — often minutes later, possibly after that turn's SSE stream already
8
- * closed. core's notifier seam + the SVC-1 {@link WorkflowNotifyGate} deliver its terminal payload to the
9
- * deployment's `deliverWorkflowCompletion` AT-LEAST-ONCE, but that was a log-only shell: the model that
10
- * launched the workflow never learned it finished (it had only the deterministic `WorkflowStatus` poll floor).
11
- *
12
- * This inbox is the PUSH half. `deliverWorkflowCompletion` resolves the originating **session + owner** (from
13
- * the payload's `sourceTaskId`) and `enqueue`s the completion here; when that session next OPENS a stream (a
14
- * new `POST /v1/runs` turn), the HTTP layer `drain`s the inbox and — for entries whose `owner` matches the
15
- * draining caller's VERIFIED principal — emits a `workflow_complete` out-of-band SSE frame (mirroring the
16
- * question/elicit frame pattern) that the shell turns into a `<task-notification>` (the shell's
17
- * `enqueuePendingNotification`, deduped by runId). Delivery is thus decoupled from whether a stream was open
18
- * at completion time — a disconnected-then-reconnected shell still gets it — WITHOUT a cross-loop append into
19
- * the originating run's own event log (its `seq` counter is owned by that run's loop; appending from outside
20
- * would race it — so completions live in this SEPARATE store).
21
- *
22
- * ## Tenancy
23
- * Each entry carries the originating run's `owner`. The drain-side owner check (in the HTTP layer) is a
24
- * defence-in-depth SECOND gate on top of the authorizer's sessionId→owner binding: a session that is deleted
25
- * then re-claimed by a different tenant rotates that binding, so the stamped `owner` is what actually gates
26
- * whether a completion is EMITTED. `purge(sessionId)` is called on session delete so orphaned completions
27
- * don't linger. (`owner: null` = the single-user turnkey posture, no tenancy boundary.)
28
- *
29
- * ## Durability / delivery model
30
- * - **At-least-once, shell-deduped**: the shell dedups by `runId` (`markTaskNotified`), so a completion frame
31
- * re-delivered across a crash is harmless. `drain` therefore removes on read; the small window where a crash
32
- * after `drain` but before the frame is flushed loses a push is covered by the deterministic `WorkflowStatus`
33
- * poll floor (core 1.197) — never a silent hang.
34
- * - **Crash-safe File ledger** (default): an append-only `(op) → record` journal, replayed into an in-memory
35
- * `sessionId → (runId → entry)` map. A torn trailing line (crash mid-append) is dropped on replay
36
- * (uncommitted). **Compacted** — rewritten from the live in-memory state on boot AND once the ledger grows
37
- * past {@link COMPACT_EVERY} appends — so tombstones/evicted/drained lines don't accumulate without bound.
38
- * Owner-only perms (identity data).
39
- * - **Bounded**: at most {@link MAX_PENDING_PER_SESSION} newest completions are retained per session; the
40
- * oldest is dropped (with a warn on LIVE enqueue — suppressed during replay to avoid log noise).
41
- */
42
1
  import { mkdirSync, readFileSync, existsSync, openSync, writeSync, fsyncSync, closeSync, renameSync } from "node:fs";
43
2
  import { join } from "node:path";
44
3
  import { redactSecrets } from "../trace/redact.js";
45
- /** Cap on retained pending completions per session (drop-oldest beyond it). A session with more than this many
46
- * undelivered workflow completions is pathological; the poll floor still covers the dropped ones. */
47
4
  export const MAX_PENDING_PER_SESSION = 100;
48
- /** Compact the File ledger (rewrite from live state) once this many records have been appended since the last
49
- * compaction — bounds on-disk size + boot replay cost for a long-lived process with high workflow churn. */
50
5
  export const COMPACT_EVERY = 512;
51
- /** How long after a `purge` an `enqueue` for the same session is refused (the purge-vs-enqueue fence): a
52
- * workflow completion landing AFTER its session was deleted must not resurrect as a live entry the next
53
- * claimant of that sessionId would see. In-memory only (a restart forgets the fence — the cross-restart race
54
- * window is negligible and the drain-side owner gate still stands as the real tenancy boundary). */
55
6
  export const PURGE_FENCE_MS = 10 * 60 * 1000;
56
- /** How long after {@link WorkflowCompletionInbox.markTerminalServed} an `enqueue` for the SAME (session,
57
- * runId) is refused — covers the notify path lagging the model's poll (the notify gate is async +
58
- * at-least-once, so the completion can land in the inbox AFTER the poll already told the model). Long enough
59
- * to swallow any realistic notify lag incl. a boot-recovery re-fire racing a fresh poll; per-runId scoped, so
60
- * it can never starve a DIFFERENT workflow's push. In-memory only (see {@link PURGE_FENCE_MS} rationale). */
61
7
  export const SERVED_FENCE_MS = 30 * 60 * 1000;
62
- /** design/144 §4 server 折叠键:`JSON([域, taskId, RAW status])[#seq]`。域=external 帧独立 lane(task_id 是
63
- * 调用方自由串,不分域可预占内部帧键把真实子代 settle 帧吞掉)/其余内部帧统一 "task" 域。
64
- *
65
- * 三路复审修1(MED):status 保留 RAW,不再归一 completed/failed —— 归一让同 id 无 seq 的两条 external
66
- * 通知(先 `event` 后 `cancelled`)折叠成同一把 `…:failed` 键,inbox map 里第二条 REPLACE 第一条,drain
67
- * 只吐一条(entry 的存储 status 列仍归一,那是账本的双终态列,不动;RAW status 本就随 payload extras
68
- * 透传)。这与 {@link taskNotificationStreamKey} 的 RAW 语义对齐(stream 侧早已保 RAW)。
69
- *
70
- * 编码=防碰撞元组:域/taskId/status 走 JSON 数组(调用方自由串伪造不了分隔符);`#seq` 尾折在 JSON 之外
71
- * ——JSON 段恒以 `"]` 结尾,`#digits` 尾无歧义,于是 {@link foldKeyFamily} 的剥尾语义 AND SQL 双生子
72
- * (workflow-run-store-sql 的 `regexp_replace(run_id, '#[0-9]+$', '')` 家族比对)原样成立——把 seq 折进
73
- * JSON 元组会让 SQL 侧家族 ack/fence 永 miss(那是另一批的真库改动面,本修有意不碰)。
74
- * seq 折入=逐停机周期各自成键(1.283 起 background_agent 四腿 notify 铸 cycleSeq);无 seq=单周期旧键
75
- * (bash/monitor terminal/retain 降级,诚实语义)。serve-fence 侧必须用同一函数(或其家族前缀)构造,
76
- * 否则键形漂移=fence 永 miss(server.ts fleet 腿的 `taskNotificationFoldKey({task_id, status})` 无 seq 形
77
- * 即家族前缀,两侧同为 RAW status,依旧咬合)。 */
78
8
  export function taskNotificationFoldKey(n) {
79
9
  const domain = n.task_type === "external" ? "external" : "task";
80
10
  return `${JSON.stringify([domain, n.task_id, n.status])}${n.seq !== undefined ? `#${n.seq}` : ""}`;
81
11
  }
82
- /** design/144 stream 侧去重键(core [784]④a + codex 1.189 前双审 F2)= `域:taskId:status[#seq]`。
83
- * 与 {@link taskNotificationFoldKey} 同族,域二分也一致(codex F2 HIGH:external 通知的 task_id 是
84
- * POST /v1/sessions/:id/notify 调用方自由串——同一条腿上,drain 出的 external 帧可与内部任务帧同形
85
- * `task_id:status:seq` 撞键互吞,per-leg 键集并不豁免这个面,旧注释「没有跨调用方预占面」判错);
86
- * 仍与 fold key 有意不同的一点:status 保留 RAW(killed/cancelled/event 不归一 failed)——归一会让同
87
- * task 的 Monitor event 批与 failed 终态在一条腿上互吞;wire 去重要的是逐帧身份,不是 inbox 的双终态列。
88
- * `seq` 折入 = 停→唤→停的第二周期同 status 帧各自成键(core 1.283 起 background_agent 四腿 notify 铸
89
- * cycleSeq;缺 seq 时旧键曾把二周期 completed 帧在 observer 臂反向吞掉——core [784]④a);无 seq 的帧
90
- * (bash/monitor terminal/retain 降级)保持单周期旧键=同键第二帧吞,诚实语义。[2373]B-10:core 4.0.0 起
91
- * task_type 是必填闭合枚举——`task_type?` 可选性**不再对应旧 core 帧**,它活着只为家族前缀构造
92
- * (server.ts fleet 腿 `taskNotificationFoldKey({task_id,status})` 同款用法);缺席归 "task" 域的语义
93
- * 只在前缀用途下生效。external 帧三来源齐备(notify 端点 core 铸 / inbox entry extras 存 / drain 帧
94
- * spread 复原),不会漏域。observer 臂 / stream 臂 / turn-open drain 登记三处必须用本函数
95
- * 构键且都传 task_type,键形漂移=去重永 miss。 */
96
12
  export function taskNotificationStreamKey(n) {
97
13
  const domain = n.task_type === "external" ? "external" : "task";
98
14
  return `${domain}:${n.task_id}:${n.status}${n.seq !== undefined ? `#${n.seq}` : ""}`;
99
15
  }
100
- /** Per-leg notified-key ledger cap (codex 1.189 前双审 F3):一条超长腿(海量互异 bg 通知)不许把键集
101
- * 撑到无界——超出删最老插入(Map 迭代序=插入序)。远高于任何真实 per-leg 通知量;被逐键的老通知
102
- * 最坏情形=同键晚到帧多写一次(at-least-once,shell 按键 dedup 兜底),绝不吞新帧。 */
103
16
  export const NOTIFIED_KEYS_MAX = 4096;
104
- /**
105
- * Per-leg delivered-notification ledger(codex 1.189 前双审 F1 HIGH 的修位)。
106
- * 键 = {@link taskNotificationStreamKey};值 = 「认领这把键的那次写是否真落盘」的 promise(true=成功)。
107
- * 为什么是 Map<string, Promise<boolean>> 而不是 Set:bg/resume 腿的 observer 臂是 fire-and-forget append
108
- * (`void append(...)`)——旧 Set 下 append 失败(SQL blip)时键已被占而盘上无帧,core pend-drain 把同
109
- * payload 重放到 stream 臂时被键吞 = 通知永丢。现在 stream 臂对已见键 `await` 认领方的真实结果,false 则
110
- * 照写并把键值升为 true。sync 腿的写是同步 res.write(无异步失败窗口)、drain 臂的写被
111
- * emitPendingWorkflowCompletions await(失败→不 ack→下腿重投)——两者登记 resolved(true) 即可;三腿统一
112
- * 用本 Map 形状,让三对臂保持逐行孪生。
113
- */
114
17
  export class NotifiedKeys {
115
18
  byKey = new Map();
116
- /** The pending/settled outcome of the write that claimed `key`, or undefined when unclaimed. */
117
19
  get(key) {
118
20
  return this.byKey.get(key);
119
21
  }
120
- /** Claim (or update) `key` with the outcome of its write. Bounded: past {@link NOTIFIED_KEYS_MAX} the
121
- * oldest-inserted key is dropped (F3; an update keeps the original insertion slot — good enough for a
122
- * pathology cap). */
123
22
  set(key, written) {
124
23
  this.byKey.set(key, written);
125
24
  while (this.byKey.size > NOTIFIED_KEYS_MAX) {
@@ -128,27 +27,15 @@ export class NotifiedKeys {
128
27
  }
129
28
  }
130
29
  }
131
- /** 家族前缀 = 折叠键只剥 `#seq` 分量(仅当尾是纯数字才剥)。新元组键的 JSON 段恒以 `"]` 结尾、taskId/
132
- * status 里天然的 # 被 JSON 引号包住,`#digits` 尾无歧义=必是 seq;旧格式遗留行(`域:task:status#seq`)
133
- * 与 workflow_complete 的裸 runId 键同一语义原样兼容。 */
134
30
  export function foldKeyFamily(runId) {
135
31
  const i = runId.lastIndexOf("#");
136
32
  return i > 0 && /^\d+$/.test(runId.slice(i + 1)) ? runId.slice(0, i) : runId;
137
33
  }
138
- /** Collision-safe (session, runId) fence key — ids are caller-influenced strings, so a plain separator could
139
- * be forged into a cross-pair collision; JSON tuple encoding cannot. */
140
34
  const servedKey = (sessionId, runId) => JSON.stringify([sessionId, runId]);
141
- /** In-memory inbox — the parity oracle + the ephemeral opt-out (WORKFLOW_RUN_STORE=memory). No persistence:
142
- * a restart forgets undelivered completions (the poll floor covers them). */
143
35
  export class InMemoryWorkflowCompletionInbox {
144
36
  onWarn;
145
37
  bySession = new Map();
146
- /** sessionId → {at, owner}: purge stamps; enqueues within {@link PURGE_FENCE_MS} for the SAME owner are
147
- * refused. Swept on BOTH enqueue and purge (a purge-heavy, enqueue-quiet process must not accumulate
148
- * stamps unboundedly — review). */
149
38
  purgedAt = new Map();
150
- /** servedKey(sessionId, runId) → stamp: terminal-poll-served fences; an enqueue for the pair within
151
- * {@link SERVED_FENCE_MS} is refused (the poll already told the model). Swept alongside the purge fence. */
152
39
  servedAt = new Map();
153
40
  constructor(onWarn) {
154
41
  this.onWarn = onWarn;
@@ -161,16 +48,9 @@ export class InMemoryWorkflowCompletionInbox {
161
48
  if (now - at > SERVED_FENCE_MS)
162
49
  this.servedAt.delete(key);
163
50
  }
164
- /** True (and warns) when `entry`'s session was purged within the fence window AND the fence applies to its
165
- * owner: same owner as the purged session (a late completion of the DELETED session) — or an owner-less
166
- * fence (caller couldn't say), conservative. A DIFFERENT owner = a new legitimate claimant of a reused
167
- * sessionId → not fenced (their push must not be starved; the drain-side owner gate stays the real
168
- * tenancy boundary either way). */
169
51
  purgeFenced(entry) {
170
52
  const now = Date.now();
171
53
  this.sweepFence(now);
172
- // Served fence FIRST (per-runId, so it can't shadow the purge fence's owner semantics): this exact
173
- // completion already reached the model through its own poll — enqueueing it would be the double delivery.
174
54
  if (this.servedAt.has(servedKey(entry.sessionId, entry.runId)) || this.servedAt.has(servedKey(entry.sessionId, foldKeyFamily(entry.runId)))) {
175
55
  this.onWarn?.("workflow_completion_enqueue_after_poll_served", { sessionId: entry.sessionId, runId: entry.runId });
176
56
  return true;
@@ -179,20 +59,14 @@ export class InMemoryWorkflowCompletionInbox {
179
59
  if (st === undefined)
180
60
  return false;
181
61
  if (st.owner !== undefined && entry.owner !== st.owner)
182
- return false; // new claimant, different owner → pass
62
+ return false;
183
63
  this.onWarn?.("workflow_completion_enqueue_after_purge", { sessionId: entry.sessionId, runId: entry.runId });
184
64
  return true;
185
65
  }
186
- /** Fence FIRST (an enqueue racing the poll is refused even mid-call), then drop any already-pending
187
- * entry via `ack` — the durable subclass inherits this and gets a durable ack for free. */
188
66
  async markTerminalServed(sessionId, runId) {
189
67
  this.sweepFence(Date.now());
190
68
  this.servedAt.set(servedKey(sessionId, runId), Date.now());
191
69
  await this.ack(sessionId, runId);
192
- // 家族 ack(design/144 消费批):fleet 常开流帧不带 seq,而 1.283 起终态 park 是 seq 键——精确 ack
193
- // 必 miss ⇒ 下次开流 drain 二次注入模型。传入 runId 视作家族前缀,清掉同 (域,taskId,status) 的全部
194
- // seq 键成员。取舍如实记:同家族的更早 Monitor 批可能被连带清(终态已送达后丢进度行,低害),
195
- // 换取每个 bg 终态必然双投的消除;workflow_complete 键无 # 尾,家族=自身,行为不变。
196
70
  const q = this.bySession.get(sessionId);
197
71
  if (q) {
198
72
  for (const key of [...q.keys()]) {
@@ -202,16 +76,14 @@ export class InMemoryWorkflowCompletionInbox {
202
76
  }
203
77
  }
204
78
  stampPurge(sessionId, owner) {
205
- this.sweepFence(Date.now()); // bound the map on the purge side too (enqueue-quiet processes)
79
+ this.sweepFence(Date.now());
206
80
  this.purgedAt.set(sessionId, { at: Date.now(), owner });
207
81
  }
208
- /** Fold one entry into the map. `warn` is false during ledger replay (a historical overflow must not re-log
209
- * on every boot) and true on a live enqueue. */
210
82
  applyEnqueue(entry, warn) {
211
83
  let q = this.bySession.get(entry.sessionId);
212
84
  if (!q)
213
85
  this.bySession.set(entry.sessionId, (q = new Map()));
214
- q.delete(entry.runId); // idempotent + re-insert at the tail (Map preserves insertion order → oldest-first)
86
+ q.delete(entry.runId);
215
87
  q.set(entry.runId, entry);
216
88
  while (q.size > MAX_PENDING_PER_SESSION) {
217
89
  const oldest = q.keys().next().value;
@@ -222,16 +94,13 @@ export class InMemoryWorkflowCompletionInbox {
222
94
  }
223
95
  async enqueue(entry) {
224
96
  if (this.purgeFenced(entry))
225
- return; // late completion of a DELETED session → drop (poll floor covers)
97
+ return;
226
98
  this.applyEnqueue(entry, true);
227
99
  }
228
100
  async pending(sessionId) {
229
101
  const q = this.bySession.get(sessionId);
230
102
  if (!q || q.size === 0)
231
103
  return [];
232
- // The served fence is armed SYNCHRONOUSLY in markTerminalServed (before its async ack
233
- // lands), so filtering here closes the same-process race where a stream-open drain reads a still-pending
234
- // entry while the ack write is in flight — the drain must never re-deliver what the fence says was served.
235
104
  return [...q.values()].filter((e) => !this.servedAt.has(servedKey(e.sessionId, e.runId)) && !this.servedAt.has(servedKey(e.sessionId, foldKeyFamily(e.runId))));
236
105
  }
237
106
  async ack(sessionId, runId) {
@@ -247,8 +116,6 @@ export class InMemoryWorkflowCompletionInbox {
247
116
  this.stampPurge(sessionId, purgedOwner);
248
117
  }
249
118
  }
250
- /** File-backed crash-safe inbox (the durable default — a background workflow's completion survives a replica
251
- * restart, the prerequisite for pushing it to a session that reconnects after the crash). */
252
119
  export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox {
253
120
  fsyncEnabled;
254
121
  dir;
@@ -259,12 +126,10 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
259
126
  super(onWarn);
260
127
  this.fsyncEnabled = fsyncEnabled;
261
128
  this.dir = join(root, "workflow-completion-inbox");
262
- // 0o700 dir + 0o600 ledger: entries carry the completion summary (may reference tenant work) + sessionId +
263
- // owner — owner-only, parity with the notify-journal's identity-data posture.
264
129
  mkdirSync(this.dir, { recursive: true, mode: 0o700 });
265
130
  this.ledgerPath = join(this.dir, "inbox.jsonl");
266
131
  this.replay();
267
- this.compact(); // rewrite the ledger to just the live pending set → tombstones/evicted lines don't survive restarts
132
+ this.compact();
268
133
  }
269
134
  replay() {
270
135
  if (!existsSync(this.ledgerPath))
@@ -278,7 +143,7 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
278
143
  rec = JSON.parse(line);
279
144
  }
280
145
  catch {
281
- continue; // torn/partial trailing line (crash mid-append) — uncommitted, skip
146
+ continue;
282
147
  }
283
148
  if (rec.op === "enqueue" && rec.entry?.sessionId && rec.entry.runId)
284
149
  this.applyEnqueue(rec.entry, false);
@@ -286,40 +151,29 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
286
151
  const q = this.bySession.get(rec.sessionId);
287
152
  q?.delete(rec.runId);
288
153
  if (q && q.size === 0)
289
- this.bySession.delete(rec.sessionId); // mirror the live ack — replay is a strict fold
154
+ this.bySession.delete(rec.sessionId);
290
155
  }
291
156
  else if (rec.op === "drain" && rec.sessionId)
292
- this.bySession.delete(rec.sessionId); // purge today; pre-1.80 drain lines too
157
+ this.bySession.delete(rec.sessionId);
293
158
  }
294
159
  }
295
160
  commit(rec) {
296
161
  if (this.fd === undefined)
297
- this.fd = openSync(this.ledgerPath, "a", 0o600); // owner-only (identity/work data)
162
+ this.fd = openSync(this.ledgerPath, "a", 0o600);
298
163
  writeSync(this.fd, `${JSON.stringify(rec)}\n`);
299
164
  if (this.fsyncEnabled)
300
- fsyncSync(this.fd); // durable BEFORE the in-memory state the caller observes
165
+ fsyncSync(this.fd);
301
166
  this.appendsSinceCompact++;
302
167
  }
303
- /** Compact iff we've appended enough since the last one. MUST be called AFTER the in-memory mutation the
304
- * triggering commit records — compact() rewrites from `bySession`, so a compact fired BEFORE the caller
305
- * applied its enqueue/delete would rewrite a stale snapshot and lose that record. */
306
168
  maybeCompact() {
307
169
  if (this.appendsSinceCompact >= COMPACT_EVERY)
308
170
  this.compact();
309
171
  }
310
- /** Rewrite the ledger from the authoritative in-memory state — one `enqueue` record per LIVE pending entry —
311
- * dropping every accumulated drain-tombstone + evicted/drained line. Atomic (temp + rename) so a crash
312
- * mid-compact leaves the old ledger intact. */
313
172
  compact() {
314
173
  if (this.fd !== undefined) {
315
174
  closeSync(this.fd);
316
175
  this.fd = undefined;
317
176
  }
318
- // tmp 名带 pid(2026-07-28 核查修):两个进程共用同一数据根时(例:两个 memory 形引擎共用一个
319
- // HOME——multi-instance 共享目录本不受支持,但 memory 形用户合理预期「无盘无锁」,却默默共享
320
- // `~/.ai-agent`),固定 `.tmp` 名会互相 rename 走对方的 staging ⇒ 对方 renameSync ENOENT,而 compact
321
- // 在 constructor 里同步跑 = **boot fatal**(真机复现:两只引擎并发 boot 恢复期)。带 pid 后各进程
322
- // staging 独立,rename 原子替换回落到 last-writer-wins(账本内容级共享仍不支持,但不再拒启)。
323
177
  const tmp = `${this.ledgerPath}.tmp.${process.pid}`;
324
178
  const lines = [];
325
179
  for (const q of this.bySession.values())
@@ -335,9 +189,7 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
335
189
  finally {
336
190
  closeSync(tfd);
337
191
  }
338
- renameSync(tmp, this.ledgerPath); // atomic replace of the ledger
339
- // fsync the DIRECTORY so the rename (a namespace change) is itself crash-durable on POSIX/macOS, not just
340
- // the file contents. Best-effort: a dir that can't be opened for fsync (rare) is skipped.
192
+ renameSync(tmp, this.ledgerPath);
341
193
  if (this.fsyncEnabled) {
342
194
  try {
343
195
  const dfd = openSync(this.dir, "r");
@@ -349,40 +201,35 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
349
201
  }
350
202
  }
351
203
  catch {
352
- /* dir fsync unsupported/failed — the rename is still namespace-atomic; skip */
353
204
  }
354
205
  }
355
206
  this.appendsSinceCompact = 0;
356
207
  }
357
208
  async enqueue(entry) {
358
209
  if (this.purgeFenced(entry))
359
- return; // late completion of a DELETED session → never re-committed to the ledger
210
+ return;
360
211
  this.commit({ op: "enqueue", entry });
361
212
  this.applyEnqueue(entry, true);
362
- this.maybeCompact(); // AFTER applyEnqueue → compact's rewrite includes this entry
213
+ this.maybeCompact();
363
214
  }
364
- // `pending` is the inherited non-destructive read — deliberately NO ledger write (phase 1 of two-phase
365
- // delivery leaves the durable state untouched; only the per-entry `ack` mutates it, so a crash/failed
366
- // frame-write between the two re-delivers on the next leg instead of silently eating the completion).
367
215
  async ack(sessionId, runId) {
368
216
  const q = this.bySession.get(sessionId);
369
217
  if (!q?.has(runId))
370
- return; // idempotent — a raced double-ack (two legs) writes ONE tombstone
371
- this.commit({ op: "ack", sessionId, runId }); // durable BEFORE the in-memory delete (deliver-then-ack)
218
+ return;
219
+ this.commit({ op: "ack", sessionId, runId });
372
220
  q.delete(runId);
373
221
  if (q.size === 0)
374
222
  this.bySession.delete(sessionId);
375
- this.maybeCompact(); // AFTER delete → compact's rewrite reflects the ack (no resurrection)
223
+ this.maybeCompact();
376
224
  }
377
225
  async purge(sessionId, purgedOwner) {
378
- this.stampPurge(sessionId, purgedOwner); // fence FIRST — an enqueue racing the purge is refused even mid-purge
226
+ this.stampPurge(sessionId, purgedOwner);
379
227
  if (!this.bySession.has(sessionId))
380
228
  return;
381
- this.commit({ op: "drain", sessionId }); // a purge clears the whole session (session-level tombstone)
229
+ this.commit({ op: "drain", sessionId });
382
230
  this.bySession.delete(sessionId);
383
- this.maybeCompact(); // AFTER delete → compact's rewrite reflects the purge
231
+ this.maybeCompact();
384
232
  }
385
- /** Close the ledger fd (shutdown). */
386
233
  close() {
387
234
  if (this.fd !== undefined) {
388
235
  closeSync(this.fd);
@@ -390,41 +237,14 @@ export class FileWorkflowCompletionInbox extends InMemoryWorkflowCompletionInbox
390
237
  }
391
238
  }
392
239
  }
393
- /**
394
- * P1 ①② delivery half, TWO-PHASE since 1.80: read the session's pending completions, emit
395
- * one `workflow_complete` out-of-band frame per entry, and `ack` (remove) each entry only AFTER its emit
396
- * SUCCEEDED — a failed frame write (dead socket / durable-append error) leaves the entry pending, so the next
397
- * leg re-delivers instead of silently eating it (the 1.77-review "drained-but-append-failed" window, closed).
398
- * Called at STREAM-OPEN of EVERY run leg (the sync `POST /v1/runs` SSE leg emits a live frame; the bg + resume
399
- * legs append a durable `workflow_complete` event their tailing client replays). Best-effort throughout (the
400
- * deterministic `WorkflowStatus` poll is the floor); at-least-once — the shell dedups by runId (two legs racing
401
- * the same session may double-emit; same contract).
402
- *
403
- * 🔒 Tenancy (defence-in-depth over the authorizer's sessionId→owner gate): only entries whose stamped
404
- * `owner` matches `callerPrincipal` are EMITTED — a session deleted-then-reclaimed by a different tenant
405
- * (which rotates the sessionId→owner binding) can't surface the prior owner's completions. `owner: null`
406
- * (single-user turnkey) has no tenancy boundary → always emitted. A non-matching entry is a DELIBERATE drop —
407
- * acked (the session is gone/reclaimed for its original owner; the poll floor is their fallback), so it never
408
- * wedges the queue.
409
- */
410
- export async function emitPendingWorkflowCompletions(inbox, sessionId, callerPrincipal, emit,
411
- /** Emit-TARGET diagnostics — WHICH leg drained (route) onto WHAT kind of connection
412
- * (live SSE write vs durable event-log append), plus the enqueue→emit lag, so a "workflow completed but the
413
- * shell only saw it on the next manual turn" report can be pinned to the leg that finally carried the frame
414
- * (the push is stream-open-driven by design — a session with NO open leg has no push path until its next
415
- * leg; the lag makes that visible instead of a mystery). Info-level, one line per delivered frame. */
416
- diag) {
240
+ export async function emitPendingWorkflowCompletions(inbox, sessionId, callerPrincipal, emit, diag) {
417
241
  if (!inbox || !sessionId)
418
242
  return;
419
- // Diagnostics must never influence delivery (1.108 review): a throwing log sink inside the try would
420
- // misroute an already-delivered-and-acked frame into the emit_failed line — a lie in the very diagnostics
421
- // this sink exists to provide. Swallow here; the drain semantics stay byte-identical with diag absent.
422
243
  const dlog = (msg, meta) => {
423
244
  try {
424
245
  diag?.log(msg, meta);
425
246
  }
426
247
  catch {
427
- /* never let diagnostics fault the drain */
428
248
  }
429
249
  };
430
250
  let entries;
@@ -432,34 +252,28 @@ diag) {
432
252
  entries = await inbox.pending(sessionId);
433
253
  }
434
254
  catch {
435
- return; // best-effort push — the model can always fall back to the WorkflowStatus poll
255
+ return;
436
256
  }
437
257
  for (const e of entries) {
438
258
  try {
439
259
  if (e.owner !== null && e.owner !== callerPrincipal) {
440
- await inbox.ack(sessionId, e.runId); // 🔒 not this caller's completion → deliberate drop (ack, never emit)
260
+ await inbox.ack(sessionId, e.runId);
441
261
  dlog("workflow_complete_dropped_owner_mismatch", { route: diag?.route, sessionId, runId: e.runId });
442
262
  continue;
443
263
  }
444
264
  if (e.kind === "task_notification") {
445
- // A background subagent/bash completion parked while the session was idle — drain it as the
446
- // SAME frame family the live legs emit (`task_notification`, task_id keyed), so the shell's existing
447
- // renderer + dedup consume it unchanged. Extras (task_type/result/…) were bounded+redacted at enqueue.
448
265
  let extras = {};
449
266
  try {
450
267
  extras = e.payload ? JSON.parse(e.payload) : {};
451
268
  }
452
269
  catch {
453
- /* a torn payload degrades to the summary-only frame */
454
270
  }
455
- // `injected: false` AFTER the extras spread:恒量判别位([1818]§四 (b)案,与 project.ts live 投影
456
- // 同义)——parked 通知更如实:park 发生恰因 leg 已死、core 的注入无处可去。
457
271
  await emit({ type: "task_notification", task_id: e.runId, status: e.status, summary: e.summary, ...extras, injected: false });
458
272
  }
459
273
  else {
460
274
  await emit({ type: "workflow_complete", runId: e.runId, status: e.status, summary: e.summary });
461
275
  }
462
- await inbox.ack(sessionId, e.runId); // phase 2: the frame durably landed → remove
276
+ await inbox.ack(sessionId, e.runId);
463
277
  dlog("workflow_complete_emitted", {
464
278
  route: diag?.route,
465
279
  connection: diag?.connection,
@@ -470,51 +284,22 @@ diag) {
470
284
  });
471
285
  }
472
286
  catch (err) {
473
- // emit failed → NOT acked → re-delivered on the next leg (at-least-once; a dead socket / failed append
474
- // must not fault the turn). An ack failure after a successful emit re-delivers too — same contract.
475
287
  dlog("workflow_complete_emit_failed", { route: diag?.route, connection: diag?.connection, sessionId, runId: e.runId, err: err instanceof Error ? err.message : String(err) });
476
288
  }
477
289
  }
478
290
  }
479
- /**
480
- * The durable half is the deployment's job: map a core `TaskNotificationPayload`
481
- * (background subagent / background bash completion) onto an inbox entry, for the IDLE gap — core's in-process
482
- * injection covers a LIVE harness; a completion that lands after the leg ended had NO durable path (idle no-show,
483
- * lost across restart). Bounded + redacted HERE (the store carries only egress-safe bytes; summary/result are
484
- * UNTRUSTED free text). `status` collapses onto the inbox's completed|failed column; the ORIGINAL status rides
485
- * the payload extras and overrides on drain (killed/cancelled reach the shell verbatim). Workflow-type
486
- * notifications are the CALLER's job to filter (they have their own durable chain — double-enqueueing here would
487
- * re-open the poll-served double-push).
488
- */
489
- export function taskNotificationInboxEntry(sessionId, owner, n, enqueuedAt,
490
- /** lead-b (dual-ID audit): the PARENT run's durable taskId (the uuid the shell's footer rows
491
- * key on) — rides the drained frame so a consumer can bind the a*-keyed child notification to its uuid-keyed
492
- * parent row without a side lookup. */
493
- parentTaskId) {
291
+ export function taskNotificationInboxEntry(sessionId, owner, n, enqueuedAt, parentTaskId) {
494
292
  const cap = (s, max) => (s.length > max ? `${s.slice(0, max)}…[+${s.length - max} chars]` : s);
495
293
  const extras = {
496
294
  task_type: n.task_type,
497
295
  ...(parentTaskId ? { parentTaskId } : {}),
498
- // core 1.238: the payload's own sessionId — the stable a*↔local-uuid cross-system anchor
499
- // (same handle TaskResult.sessionId/fleet rows carry). Optional (bash lane has none).
500
296
  ...(n.sessionId ? { sessionId: n.sessionId } : {}),
501
297
  ...(n.toolUseId ? { toolUseId: n.toolUseId } : {}),
502
- ...(n.status !== "completed" && n.status !== "failed" ? { status: n.status } : {}), // original rides through
503
- // core drain 案: the Monitor/stoppedBy fields must survive the PARK too, or a drained event batch
504
- // arrives with no dedup discriminator (`seq` — core's taskNotificationDedupKey folds it in), no line text,
505
- // and no stop attribution. Same trust posture as the live projection (project.ts): seq/stoppedBy verbatim
506
- // (core counter / open enum), lines = watched-process stdout → redact + cap each line.
298
+ ...(n.status !== "completed" && n.status !== "failed" ? { status: n.status } : {}),
507
299
  ...(n.seq !== undefined ? { seq: n.seq } : {}),
508
- // 对抗评审 2026-07-11:redact BEFORE cap — capping first could bisect a secret so its residue no longer
509
- // matches any redact pattern (a half-token is still a usable prefix). Order: redact, then bound.
510
300
  ...(n.lines !== undefined ? { lines: n.lines.map((l) => cap(redactSecrets(l), 1024)) } : {}),
511
301
  ...(n.stoppedBy !== undefined ? { stoppedBy: n.stoppedBy } : {}),
512
- ...(n.diagnostics !== undefined ? { diagnostics: cap(n.diagnostics, 4_000) } : {}), // [1412] 诊断槽(bounded;引擎值单源,park 原样过)
513
- // (core 1.262.0 残局字段的 PARK 半场——live 腿 1.154.0 已放行,但「用户杀 bg 子代」的
514
- // 主动线恰是 between-turns,通知落这里):与 project.ts 同一 trust posture——recentSteps.tool verbatim,
515
- // target/outcome UNTRUSTED(命令行/工具输出首行可为 secret 形)→ redact+cap;editedFiles.path → redact+cap
516
- // (attachedFiles 姿势),edits 数字 verbatim;resumable boolean verbatim。字段挑取绝不 spread;数组防御
517
- // 截断(core 上游本就 bound ≤10 步——slice 是入库前的双保险)。
302
+ ...(n.diagnostics !== undefined ? { diagnostics: cap(n.diagnostics, 4_000) } : {}),
518
303
  ...(n.recentSteps !== undefined
519
304
  ? { recentSteps: n.recentSteps.slice(0, 10).map((s) => ({ tool: s.tool, target: cap(redactSecrets(s.target), 512), outcome: cap(redactSecrets(s.outcome), 512) })) }
520
305
  : {}),
@@ -528,43 +313,14 @@ parentTaskId) {
528
313
  return {
529
314
  sessionId,
530
315
  owner,
531
- // 对抗评审 2026-07-11(MED):the inbox key is REPLACE-on-runId (the workflow_complete contract) — keying a
532
- // task_notification by the bare task_id COLLAPSES multiple between-turns notifications of the same bg task
533
- // (a Monitor event batch, then the killed terminal: the terminal REPLACED the batch, drain emitted 1 of 2).
534
- // Fold `seq` into the key (core's own taskNotificationDedupKey folds seq for exactly this reason); the frame's
535
- // task_id comes from the extras override below, so the wire shape is unchanged. Same-key retries still dedup.
536
- // design/144 §4 键升级:(域,taskId,status,seq?)——status 进键且保 RAW(修1:归一曾让同 id 的
537
- // event/cancelled 两条 external 通知互吞),external 分域([769]①),seq 折入(逐周期)。
538
- // wire 形状不变(task_id 从 extras override 恢复);存储 status 列(下一行)仍归一双终态,不受键影响。
539
316
  runId: taskNotificationFoldKey(n),
540
317
  status: n.status === "completed" ? "completed" : "failed",
541
318
  summary: cap(redactSecrets(n.summary ?? ""), 4000),
542
319
  enqueuedAt,
543
320
  kind: "task_notification",
544
- payload: JSON.stringify({ ...extras, task_id: n.task_id }), // task_id rides extras → drain's spread restores it over the keyed runId
321
+ payload: JSON.stringify({ ...extras, task_id: n.task_id }),
545
322
  };
546
323
  }
547
- /**
548
- * Resolve WHERE a workflow completion routes (core 1.208): the SESSION comes straight off
549
- * `originatingSessionId` (the Runner closes over it at RunWorkflow mount — lookup-free). The OWNER comes from,
550
- * in order:
551
- * 1. the originating RUN ROW's owner (`getRun(sourceTaskId)`, 1.75) — immutable, stamped at submit. Only the
552
- * BG leg has a run-row `sourceTaskId` though: the sync + resume legs deliberately leave `spec.taskId` unset
553
- * (subagent nesting), so core derives `sourceTaskId = sessionId` and this lookup MISSES for them (review
554
- * review, 1.78) — which under the old rule silently killed the multi-tenant push for every sync-leg workflow;
555
- * 2. the payload's `principal` — core threads `spec.principal`, which in THIS service is ALWAYS the VERIFIED
556
- * identity (the 1.55 F-fix invariant, main.ts: gated door = BFF-verified header, direct door = JWT `sub`,
557
- * never the spoofable header; core 1.187 hardened the `TaskSpec.principal` contract to exactly this).
558
- * Captured at SPAWN time → immutable, no delete-then-reclaim window. This is also the same value core's own
559
- * workflow scope guard (`canAccessWorkflowRun`) keys off;
560
- * 3. neither → single-user stamps `owner: null` (no tenancy boundary); multi-tenant (`requirePrincipal`)
561
- * returns undefined = DROP fail-closed (an unverifiable owner must not ride the reclaim gate; the
562
- * `WorkflowStatus` poll floor covers). Returns undefined when there is no route at all.
563
- * The `getRun` lookup also carries the session FALLBACK for a crash-RECOVERY notify payload that lacks
564
- * `originatingSessionId` — since core 1.210 the durable `WorkflowRun` records it and the recovery
565
- * re-derivation threads it through (workflow-notify-journal), so this fallback covers only pre-1.210 rows
566
- * (or a writer that omitted it), not the recovery path wholesale.
567
- */
568
324
  export async function resolveCompletionRoute(p, getRun, requirePrincipal) {
569
325
  let sessionId = p.originatingSessionId;
570
326
  let owner;
@@ -573,31 +329,23 @@ export async function resolveCompletionRoute(p, getRun, requirePrincipal) {
573
329
  const originating = await getRun(p.sourceTaskId);
574
330
  if (originating) {
575
331
  sessionId ??= originating.sessionId ?? undefined;
576
- owner = originating.owner ?? null; // the run row's owner — stamped at submit, immutable
332
+ owner = originating.owner ?? null;
577
333
  }
578
334
  }
579
335
  catch {
580
- // lookup outage: fall through — the session may still route via the payload; owner resolves below
581
336
  }
582
337
  }
583
338
  if (!sessionId)
584
- return undefined; // no route → poll floor
339
+ return undefined;
585
340
  if (owner === undefined && p.principal !== undefined)
586
- owner = p.principal; // VERIFIED spec.principal (see doc #2)
341
+ owner = p.principal;
587
342
  if (owner === undefined) {
588
343
  if (requirePrincipal)
589
- return undefined; // multi-tenant + no verifiable owner → fail-closed (poll floor)
590
- owner = null; // single-user turnkey: no tenancy boundary
344
+ return undefined;
345
+ owner = null;
591
346
  }
592
347
  return { sessionId, owner };
593
348
  }
594
- /**
595
- * core 1.232 — resolve WHICH session's inbox entry a poll-served ack targets: the payload's
596
- * `originatingSessionId` directly (core threads it on `ackServed`), else the originating run row via
597
- * `sourceTaskId` (mirrors {@link resolveCompletionRoute}'s session half — same fallback, same reason:
598
- * older runs may predate the lookup-free field). Returns undefined when there is no attribution — the
599
- * entry stays pending (at-least-once; the shell's own dedup covers a rare duplicate).
600
- */
601
349
  export async function resolveServedSession(p, getRun) {
602
350
  if (p.originatingSessionId)
603
351
  return p.originatingSessionId;
@@ -606,7 +354,6 @@ export async function resolveServedSession(p, getRun) {
606
354
  return (await getRun(p.sourceTaskId))?.sessionId ?? undefined;
607
355
  }
608
356
  catch {
609
- /* lookup outage → no attribution; the poll floor + shell dedup cover */
610
357
  }
611
358
  }
612
359
  return undefined;