@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
@@ -5,7 +5,7 @@ import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTermi
5
5
  import { defaultSubagentTailBus, projectTailFrame } from "../../fleet/subagent-tail-bus.js";
6
6
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
7
7
  import { createLedgerSink } from "../../trace/ledger-sink.js";
8
- import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js"; // #310:通告 wire 腿(sync stream 半场)
8
+ import { registerEngineNoticeLeg } from "../../trace/engine-notice-wire.js";
9
9
  import { clearTurnActivity, readTurnActivityMs, recordTurnActivity } from "../../turn-activity.js";
10
10
  import { redactSecrets } from "../../trace/redact.js";
11
11
  import { contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, wiringManifestOperatorEventData, humanInputEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
@@ -18,45 +18,14 @@ import { createApprovalCardEmitter, resolveApprovalLeg, resolveStreamApprovalGat
18
18
  import { buildActiveRunConflict, toDoneFrameResult } from "../active-run-conflict.js";
19
19
  import { headerStr, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
20
20
  import { failOpenTagForDroppedFrame, recordFailOpen } from "../../observability/fail-open.js";
21
- /** #135([C95]/[C96]):live 腿 SSE 数据帧的**唯一**出口 —— 载荷带账本可锚 `eventId`(非空 string)的帧
22
- * 前缀 `id: <eventId>` 行(SSE 游标=账本 eventId,壳/SDK 拿它锚进账本或断线换道 events tail 续读);
23
- * 无 eventId 的帧**不发行**,EventSource 沿用上一游标 —— 绝不合成占位 id(那会把不可锚的位置伪装成可锚)。
24
- * durable 腿的 `id:` 行在 sse-log.ts(同一契约的另半);行为钉=test/live-sse-cursor.test.ts(规则式全流判据)。 */
25
- /** #241([3731] 裁定 A):断连转 park 的有界宽限——ask 已被强转 park 路由后,给 core 从「收到
26
- * "unavailable"」走到「铸 checkpoint + done(suspended)」的时间。到点仍没收到 done ⇒ 照旧 abort,
27
- * 交断连支既有的 findPendingTokenBySession 竞态卫兵兜底(checkpoint 已落盘则仍 park)。
28
- * 取值:core 的 park 是本地 IO(checkpoint 写盘/写库)+一次 done 帧,秒级富余;不设旋钮——它不是
29
- * 行为面选择,只是竞态兜底的等待上限(设长了唯一代价是极端卡死时 abort 晚到)。 */
30
21
  const DISCONNECT_PARK_GRACE_MS = 10_000;
31
22
  function sseData(res, payload) {
32
23
  const anchor = payload.eventId;
33
24
  const idLine = typeof anchor === "string" && anchor.length > 0 ? `id: ${anchor}\n` : "";
34
25
  res.write(`${idLine}data: ${JSON.stringify(payload)}\n\n`);
35
26
  }
36
- /**
37
- * 同步提交腿的**唯一**应答口:`retryAfterSec` 在体里 ⇒ 标准 `retry-after` 头同值在头里。
38
- *
39
- * 🔴 为什么必须是同一个口(复审车 C,2026-08-05):`memory.admission_required`(503)与
40
- * `usage.window_exhausted`(429)是同族「等一会儿再来」,而后者在 `http/server.ts` 的 pre-admission 门里
41
- * **头体双给**。本腿此前只给体——但**自动重试引擎读的是头**:SDK `Transport.request` 取
42
- * `parseRetryAfter(res.headers.get("retry-after"))`,退避睡 `retryAfterMs ?? backoff(attempt)`,而 503 在
43
- * `RETRYABLE_STATUS` 内。头缺席 ⇒ 引擎回落 250ms/500ms 通用退避,默认 `maxRetries: 2` 的三次尝试**全部**
44
- * 落在 core 的不可用窗内(core 侧 `Math.max(backoffMs, 1000)`),提交方在 1 秒内烧光重试预算然后报错 ——
45
- * 这个提示存在的唯一目的正好被绕过。头体同源(体有才给、值逐字相同)也让「体说等 11 秒、头说别的」不可能。
46
- *
47
- * 放在应答口而不是铸体处:同步腿的 4xx/5xx **不进** idem 缓存(谓词 `status < 400`),但并发同键的调用方会
48
- * 被 `peek` 喂在飞 promise 的**同一份**结果 —— 那条腿不重跑铸体处的代码,只走这里。
49
- */
50
27
  function sendSubmitResult(res, resp) {
51
28
  const body = resp.body;
52
- // 两个判定各司其职,都不能省:`Object.hasOwn` 管**语义**——`in` 认原型链,而 `JSON.stringify` 只序列化
53
- // 自有可枚举键,只用 `in` 就存在「头说等 11 秒、体里根本没这个键」的形(继承来的键足以点亮头);`in` 管
54
- // **窄化**——本仓 lib 里的 `Object.hasOwn` 没有类型谓词重载,少了它就得写断言(边界禁项)。
55
- // 🔴 诚实边界:这两个判定合起来**不等于**「JSON 里一定看得见」的通用证明(不可枚举自有键、数组上的具名键
56
- // 都能同时通过而不被序列化)。本函数的真保证是**受控生产者**级的:本腿流经这里的应答体形状不止一种
57
- // (200 的 `stripCheckpointToken(result)` 是引擎对象、409 是冲突材料……),而其中**唯一**带
58
- // `retryAfterSec` 的是下面那个 memory.admission_required 臂里的对象字面量。写死这条而不是宣称一个更强的
59
- // 不变式 —— 宣称过强正是「过期声明」的铸法。
60
29
  if (typeof body === "object" && body !== null && Object.hasOwn(body, "retryAfterSec") && "retryAfterSec" in body) {
61
30
  const sec = body.retryAfterSec;
62
31
  if (typeof sec === "number" && Number.isFinite(sec))
@@ -64,20 +33,12 @@ function sendSubmitResult(res, resp) {
64
33
  }
65
34
  sendJson(res, resp.status, resp.body);
66
35
  }
67
- /**
68
- * [3979] §2 发现性指路:`POST /v1/tasks` 收 verify/cascade 不拒、多尝试语义照常执行,但该同步形
69
- * **没有事件通道**(契约 §4a-sext 成文;#253 的裁定=不是缺座位,是端点形状)。test 黑盒撞到
70
- * 「200 却零事件回放」证明成文≠可发现,所以带 verify/cascade 的 200 体附本句(不改行为)。
71
- * 注入点在 runSyncSubmitLeg 的三处 200 return(idem 缓存体自带 ⇒ peek 重放腿同形,不必二处注入)。
72
- */
73
36
  const MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE = "verify / cascade ran with their normal multi-attempt semantics, but the synchronous /v1/tasks endpoint has no event channel — no status/progress frames exist to replay on this shape. For observable events submit via POST /v1/runs and read GET /v1/runs/:id/events.";
74
37
  export async function handleTasks(req, res, url, ctx) {
75
38
  const miss = { fell: false };
76
39
  await handleTasksBody(req, res, url, ctx, miss);
77
40
  return !miss.fell;
78
41
  }
79
- /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(A9 的唯一改动:去缩进;B3 的唯一改动:
80
- * 给两个匿名 `idemCache.run` 回调与三条驱动腿起名)。裸 `return;` = 「本域已应答」;走到函数尾才是「没匹配上」。 */
81
42
  async function handleTasksBody(req, res, url, ctx, miss) {
82
43
  const { deps } = ctx;
83
44
  const { idemCache, inflightRuns, cancelledViaVerb, steerableRuns, counters } = ctx.registry;
@@ -86,27 +47,17 @@ async function handleTasksBody(req, res, url, ctx, miss) {
86
47
  const reqState = ctx.req;
87
48
  const source = ctx.req.source;
88
49
  if (req.method === "POST" && (url === "/v1/tasks" || url === "/v1/tasks/stream")) {
89
- // Idempotency-Key replay BEFORE the rate/quota gates (council blocker): a retry with a known key must get
90
- // the SAME response, not a fresh 429 because the window filled meanwhile. No-op without a key / on a miss.
91
50
  const rawIdem = headerStr(req.headers["idempotency-key"]);
92
- // Scope by identity only when there IS a key (BL-3) — avoids reading the principal header on every
93
- // submit, and keeps the no-key path independent of principal config.
94
- // F-fix: scope on the VERIFIED principal (gatedPrincipal) — on a direct door a spoofable header would let a
95
- // caller poison/collide another principal's idempotency cache. (BFF/gated: identical to principalFrom.)
96
51
  const idemKey = rawIdem ? scopedIdempotencyKey(rawIdem, source, gatedPrincipal(req, deps.config)) : undefined;
97
52
  const cached = idemKey ? idemCache.peek(idemKey) : undefined;
98
53
  if (cached) {
99
54
  if (url === "/v1/tasks/stream") {
100
- // The cached entry may be IN-FLIGHT (a retry arriving during the original stream) → set up the heartbeat
101
- // BEFORE awaiting so a long wait doesn't idle-timeout, then replay the terminal result.
102
55
  sseHeaders(res);
103
56
  const hb = setInterval(() => {
104
57
  if (!res.writableEnded)
105
- res.write(`event: heartbeat\ndata: {}\n\n`); // real frame (not an SSE comment) — a per-frame-parsing BFF drops comments, so downstream saw a zero-frame window; EventSource clients without a heartbeat listener ignore it (zero break)
58
+ res.write(`event: heartbeat\ndata: {}\n\n`);
106
59
  }, 15_000);
107
- hb.unref?.(); // #131-5①:同文件 durableHeartbeat 口径——停机时心跳不拖事件循环
108
- // #131-5②:重试客户端断连即清——`cached` 可悬到原始流 settle(deadline 级时长),此前这个
109
- // timer 一直空转到那时(写有 writableEnded 守卫,烧的是 tick 本身);finally 仍是兜底清。
60
+ hb.unref?.();
110
61
  res.on("close", () => clearInterval(hb));
111
62
  try {
112
63
  const resp = await cached;
@@ -120,7 +71,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
120
71
  }
121
72
  else {
122
73
  const resp = await cached;
123
- sendSubmitResult(res, resp); // 头体同源(见 sendSubmitResult 顶注:并发同键的这条腿不重跑铸体处)
74
+ sendSubmitResult(res, resp);
124
75
  }
125
76
  return;
126
77
  }
@@ -129,65 +80,31 @@ async function handleTasksBody(req, res, url, ctx, miss) {
129
80
  const prepared = await prepareSpec(req, res);
130
81
  if (!prepared)
131
82
  return;
132
- // withPrincipal threads the caller's identity through the run so the cost tracer attributes spend
133
- // (incl. council/team sub-tasks) to this principal's cumulative quota.
134
83
  const principal = prepared.auth?.principal;
135
84
  if (url === "/v1/tasks/stream") {
136
85
  if (prepared.verify || prepared.cascade) {
137
86
  sendError(res, 400, "request.field_conflict", "verify / cascade are not supported on /v1/tasks/stream (both are multi-attempt, not a single stream) — use /v1/tasks or /v1/runs");
138
87
  return;
139
88
  }
140
- // R8 (CC parity, fixed for the AgentEvent contract — workflow re-review #2): mint the durable rewind
141
- // handle (taskId) BEFORE the SSE headers so it rides as an `X-Task-Id` RESPONSE HEADER. The shell reads it even
142
- // on a Ctrl+C before `done`, WITHOUT an out-of-contract `run_started` data frame (the SDK AgentEvent stream is a
143
- // CLOSED oneOf; a synthetic data frame makes exhaustive consumers assertNever-throw). createRun below CLAIMS this id.
144
89
  const earlyDurableTid = deps.runStore && prepared.spec.sessionId ? uuidv7() : undefined;
145
- // [854]①b per-request opt-in 断连宽限:请求头 `x-detach-on-disconnect: true`(选头不选 body 字段——
146
- // 这是本条连接的传输层语义,不该进持久化 body / 不该随 resume 重放;与 idempotency-key 同类)。
147
- // 默认行为不动:交互 Esc = 断连即杀的反 token-burn 契约保留(下方 onDisconnect 的 abort 路径)。
148
- // opt-in 后 client 断连不 abort,run 转后台跑完,结果按既有 durable 路径可查(X-Task-Id → GET /v1/runs/:id)。
149
- // 无 durable 账本(无 runStore 或无 sessionId)时 400 fail-loud 而非静默忽略:忽略 = 断连仍杀 run,
150
- // 与 caller 声明的语义相反;就算不杀,跑完的结果也无处可查(纯烧 token)—— body.model silent-drop 同类,
151
- // 「不做临时方案」判 400 是真版。SSE 头未发,还能干净 400。
152
90
  const detachOnDisconnect = headerStr(req.headers["x-detach-on-disconnect"]) === "true";
153
91
  if (detachOnDisconnect && earlyDurableTid === undefined) {
154
92
  sendError(res, 400, "request.precondition_unmet", "x-detach-on-disconnect requires a durable run (a run store + sessionId) — without one the detached result would be unqueryable");
155
93
  return;
156
94
  }
157
- reqState.streamTaskId = earlyDurableTid; // [854]⑥: 499 断连日志带 task 上下文(record 只在 aborted 行读它)
158
- // E8 双腿对齐:detach 腿的 modelUsage 归因键(sessionId 域)——声明在此层,leak-fence clear 在本层 finally。
95
+ reqState.streamTaskId = earlyDurableTid;
159
96
  let usageKey;
160
- // [854]①b/⑥:opt-in 一被受理就置 detached 标记(而非等断连时置)——record 的 res 'close' listener
161
- // 注册在 onDisconnect 之前,断连时 record 先跑;在这里置位才能让 499 行真带上 detached:true。
162
- // 语义不变:opt-in 受理后的任何断连都是 detach(run 不杀)。
163
97
  reqState.streamDetached = detachOnDisconnect;
164
98
  sseHeaders(res, earlyDurableTid ? { "x-task-id": earlyDurableTid } : undefined);
165
- // G15([1833]/[1840]§五):taskId 一等信号进流首帧——浏览器 EventSource 与部分代理面读不到响应头,
166
- // 消费端此前只能靠 x-task-id(cli liveClient 亲测取头)。meta 帧与头同源同条件(durable 腿才有 id,
167
- // 无 durable ⇒ 头/帧同缺席,不铸空值);sessionId 同帧(server 铸新 session 时 caller 需要它绑定)。
168
- // additive:未知 event 型按 SSE 语义被旧消费端忽略。
169
99
  if (earlyDurableTid) {
170
100
  res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", taskId: earlyDurableTid, ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}) })}\n\n`);
171
101
  }
172
- // Abort the underlying run (and stop the heartbeat) when the client disconnects — otherwise a dropped
173
- // relay keeps burning billable tokens to completion (council). Mirrors the closed-flag pattern of the
174
- // poll-based SSE endpoints, and additionally cancels the generator via spec.signal.
175
102
  const ac = new AbortController();
176
- // 🔴 stoppedBy 归因的注册点在**下面**(createRun 认领成功之后),不在这里 —— #168 件3,见那里的注。
177
103
  let closed = false;
178
- let detachLogged = false; // [854]①b:detach 断连 info 只打一条(req/res 两个 close listener 都可能进来)
179
- // #241([3731] 裁定 A):断连转 park 的宽限表——非空 = 已触发过 ask 转 park,断连支不再二次进臂。
104
+ let detachLogged = false;
180
105
  let disconnectParkGrace;
181
106
  const onDisconnect = () => {
182
- // [854]①b:detach opt-in 的断连分支 —— 不置 closed(for-await 继续消费到 done,终态走既有
183
- // setTerminal 路径落 durable 账本)、不 abort(run 不杀)。断连后的 res.write 落在已 destroy 的
184
- // socket 上被 Node 丢弃;防御:挂一个 res 'error' swallow(destroy 后的 write 在部分 Node 版本
185
- // 经 nextTick 发 ERR_STREAM_DESTROYED,无 listener 会变 uncaught)—— 只在 detach 已断连后挂,
186
- // 正常车道的 res 错误面不被吞。task_notification 的 deliverable 谓词含 res.destroyed,断连后
187
- // 自动改走 durable inbox(既有行为,正合 detach 语义)。
188
107
  if (detachOnDisconnect) {
189
- // 正常完成后的 req 'close' 不是断连(默认车道靠 abort-after-done 无害才容得下无 guard 的
190
- // req listener;detach 车道要防它打出误导的 detached info)。
191
108
  if (res.writableEnded)
192
109
  return;
193
110
  if (!detachLogged) {
@@ -197,17 +114,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
197
114
  }
198
115
  return;
199
116
  }
200
- // #241([3731] 裁定 A「park 应跨壳死存活」,[3730] core 零改动承运;cli [3729] L3 真机复现=本臂
201
- // 缺席时的行为):断连瞬间本 run 有**流内未决 ask** 且 park 设施在场 ⇒ 不立即 abort——先把 ask 按
202
- // 窗到期同路强转 park(coordinator 交 `"unavailable"` ⇒ core 走 durable park ⇒ done(suspended)),
203
- // 给 core 一个**有界宽限**走完 park:宽限内 done 到 ⇒ 循环自然收束、走上面的 setSuspended 臂
204
- // (行 park、卡存活、resume 重呈闭环);宽限用尽(park 卡在半路)⇒ 照旧 closed+abort,下面既有的
205
- // findPendingTokenBySession 竞态卫兵接手(checkpoint 已落盘则仍 park)。真算力中断(无未决 ask)
206
- // 语义**不变**:立即 abort——那是这个断连臂存在的原始理由(dropped relay 不烧 token)。
207
117
  if (deps.checkpointStore && earlyDurableTid && disconnectParkGrace === undefined) {
208
118
  const parked = deps.toolApproval?.parkOpenAsksForTask(earlyDurableTid) ?? 0;
209
119
  if (parked > 0) {
210
- res.on("error", () => undefined); // 断连后循环继续消费,死 socket 上的 write 错误按 detach 同形吞
120
+ res.on("error", () => undefined);
211
121
  deps.logger?.info("stream_disconnect_ask_parked", { taskId: earlyDurableTid, sessionId: prepared.spec.sessionId, asks: parked, graceMs: DISCONNECT_PARK_GRACE_MS });
212
122
  disconnectParkGrace = setTimeout(() => {
213
123
  closed = true;
@@ -221,54 +131,13 @@ async function handleTasksBody(req, res, url, ctx, miss) {
221
131
  ac.abort();
222
132
  };
223
133
  req.on("close", onDisconnect);
224
- // 🔴 req 'close' does NOT fire on a client disconnect once the request body has been fully consumed
225
- // (prepareSpec read it before we get here; verified against node 24) — only res 'close' does. With only
226
- // the req listener the disconnect-abort above never fired on this route, i.e. the dropped-relay token
227
- // burn it exists to stop went right on burning. writableEnded guards the normal-completion close
228
- // (end() already called → that close is not a disconnect).
229
134
  res.on("close", () => {
230
135
  if (!res.writableEnded)
231
136
  onDisconnect();
232
137
  });
233
- // 注册后回看(合并码扫 C2,2026-08-13;判据与 src/http/sse-lifecycle.ts:76 同源,理由见其顶注
234
- // 「为什么注册完还要回看一眼状态」):本腿的注册点排在 prepareSpec/门族若干 await 之后,Node 的
235
- // 'close' 只发一次、后装监听器不补发——那段窗内断掉的连接若不回看,ac 永不 abort,整条 run 对着
236
- // 死连接烧完(dropped-relay token burn 正是这套接线存在的原始理由,被自己的注册时序绕过)。
237
- // writableEnded 卫兵同上一行:正常收流的 close 不是断连。
238
138
  if ((res.destroyed || res.closed) && !res.writableEnded)
239
139
  onDisconnect();
240
- // #154 件一:本连接的 `wiring_manifest` 帧投哪一形。判据 = `explicitOperatorOk`(**不是** `isOperator`:
241
- // 空 `OPERATOR_PRINCIPALS` 必须是「没有人是 operator」;把它读成「所有人都是」会让未配名单的部署把
242
- // 治理面发给每一个订阅者)。**每连接求一次**、不逐帧求:身份在一条连接内不会变,而逐帧调用会让
243
- // 每帧都重跑一遍凭证解析 —— 更重要的是,身份只在这一处判,帧循环里没有第二个可漂的判据。
244
140
  const wiringOperatorFace = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
245
- // #151 车3 §5.1:sync 腿的**开流重放**——`sseHeaders` + meta 帧之后、`streamBody` 之前,把同一
246
- // (owner, session) 的未决审批卡投成 `approval_request` preamble 帧。
247
- //
248
- // 🔴 位置:必须排在**上面那套断连接线之后**(codex 复审 2026-08-06 的 high)。Node 的 `'close'`
249
- // 只发一次 —— preamble 的 ≤2s 等待窗若插在接线之前,窗内断连的 close 事件在监听器注册前就烧掉了,
250
- // `closed` 恒 false、`ac` 从不 abort ⇒ 模型腿在一条已断的连接上照常起跑并烧 token,「断连即杀 run」
251
- // 这条既有契约在窗口内失效。排在接线之后 = 窗内断连照常 abort,腿拿到的是已 abort 的 signal
252
- // (与「断连发生在起跑之后」同一条既有路径,不新增早退分支)。
253
- //
254
- // 🔴 为什么按 **session** 读而不是按 taskId:sync 腿每次开流都是**新 taskId**;它要接的未决卡来自
255
- // 「同一 (owner, session) 的上一条腿 / 长命 bg 子代」——正是 `boundAsk` broker 的
256
- // `["session", owner, sessionId]` 键域。按 taskId 读永远读不到它们。
257
- //
258
- // `sessionId` 缺席的 adhoc 流**不重放**:那条腿的行上 task_id/session_id 都是客户端永远看不到的
259
- // 一次性 uuid,没有「重连回同一条腿」的概念(§5.1 表:这不是缺陷,是该腿形本来的语义)。
260
- //
261
- // owner = **verified** principal(与下方 `askOwner`、与 respond 的 owner gate 同一姿势)——重放面是
262
- // 跨腿投递,租户门必须带上,不能让 A 租户的开流看到 B 租户的卡。
263
- // 有界(2s)+ fail-open:store 抛或挂起都只记一次 warn、零卡继续开流(§5.3)。
264
- // 🔴 开关谓词与 runs.ts 同源同理由(codex 复审第二轮 high:回滚下表里可能有残留 STREAM_PENDING
265
- // 行,无谓词会破「开关关 = 逐字零变化」;顺带省掉关闭态每次开流的一次 store 往返)。钉:场景9。
266
- //
267
- // 🔴 A-010.7 随修(**兄弟扫查**:台账只点名了 runs.ts 那一处,这里是同形的第二处):谓词是
268
- // **上场门**不是裸开关。五项合取(协调器在场 / 开关真 / backend 在场 / 账本非易失 / park 设施在场)
269
- // 里少查三项 ⇒ 一个协议根本没上场的 SQL 部署,每次开流仍要白打一次库;回滚时更坏 —— 别的副本
270
- // 落下的残留 `STREAM_PENDING` 行会被无门的重放腿照常投上 wire。换成与协调器注入 / 能力面 /
271
- // 回决 501 口**同一个**谓词,「说没上场」⟺「零帧且零 store 读」才是结构成立的。
272
141
  const replayGate = resolveStreamApprovalGate({
273
142
  toolApprovalEnabled: Boolean(deps.toolApproval),
274
143
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
@@ -290,57 +159,24 @@ async function handleTasksBody(req, res, url, ctx, miss) {
290
159
  deps.logger?.warn?.("approval_preamble_capped", { sessionId: replaySessionId, dropped, replayMax: deps.config.streamApproval.replayMax, face: "tasks.stream" });
291
160
  return buildApprovalPreambleSseFrames(frames);
292
161
  }, (reason, err) => deps.logger?.warn?.("approval_preamble_failed", { sessionId: replaySessionId, reason, face: "tasks.stream", error: err instanceof Error ? err.message : String(err ?? "") }));
293
- // 具名事件、**无 `id:` 行**:preamble 不是账本行,不参与 live 腿的 eventId 游标(sseData 的契约)。
294
- // 等待窗里断连了就别再写(与心跳的 `!res.writableEnded && !res.destroyed` 同一判据)。
295
162
  if (!res.writableEnded && !res.destroyed) {
296
163
  for (const f of preambleFrames)
297
164
  res.write(`event: ${f.event}\ndata: ${JSON.stringify(f.data)}\n\n`);
298
165
  }
299
166
  }
300
- // Heartbeat so a long no-token gap (e.g. a debate phase) doesn't trip a mesh/ingress idle timeout on
301
- // the relay (center). SSE comment lines are ignored by EventSource but keep the socket alive.
302
167
  const hb = setInterval(() => {
303
- // [854]①b: `!res.destroyed` — detach 断连后 run 可能再跑很久,别每 15s 往已死 socket 写心跳
304
- // (destroyed = client 已断,写了也只是被丢弃/触发 error;默认车道断连即 abort,窗口极短,不受影响)。
305
168
  if (!res.writableEnded && !res.destroyed)
306
- res.write(`event: heartbeat\ndata: {}\n\n`); // real frame (not an SSE comment) — a per-frame-parsing BFF drops comments, so downstream saw a zero-frame window; EventSource clients without a heartbeat listener ignore it (zero break)
169
+ res.write(`event: heartbeat\ndata: {}\n\n`);
307
170
  }, 15_000);
308
- hb.unref?.(); // #131-5①:同文件 durableHeartbeat 口径——停机时心跳不拖事件循环
309
- // Wrap the whole stream in idemCache.run so the IN-FLIGHT promise is stored BEFORE work begins — a retry
310
- // that arrives WHILE this stream is still running (relay timeout → re-send same key) is then caught by the
311
- // peek above and replays this result instead of starting a second billable stream (council). `ranLive`
312
- // tells the caller that actually produced the stream (wrote events live) apart from a concurrent caller the
313
- // cache deduplicated — the latter got no live events, so it must be handed the terminal result.
171
+ hb.unref?.();
314
172
  let ranLive = false;
315
173
  let resp;
316
- // ── B3 ── 原文此处是 `idemCache.run` 的第二个实参、一个 **703 行匿名 async**。提为具名闭包:
317
- // plain-stream 腿。捕获的自由变量(`ranLive`/`prepared`/`principal`/`res`/`ac`/`closed`/`hb`/
318
- // `earlyDurableTid`/`usageKey`/`reqState`…)与原匿名回调完全同一批,故行为逐字。
319
174
  const runStreamSubmitLeg = async () => {
320
175
  ranLive = true;
321
176
  let finalResult;
322
- // [868]③: true once the post-stream settle block below recorded the run row's park/terminal state.
323
- // A throw OUT of the stream (abort mid-iteration, or any plain Error before `done`) skips that whole
324
- // block — the finally's backstop then settles the leaked `running` row (the durable twin of the
325
- // fleet-row leak C fixed below; the fleet half got its settleFleet("failed") backstop, the run row
326
- // was left to reapStale's stale-timeout with a misleading "run stalled" verdict).
327
177
  let runRowSettled = false;
328
- // Durable F4/ask on the stream path. Two things the sync/async paths do that the stream path
329
- // used to skip (audit functional gap; both caught live on the gate#3 P3 cross-replica drill):
330
- // 1. putCtx BEFORE the run → a suspend here (e.g. an AskUserQuestion gate, TC-5.4) is resumable
331
- // on any replica; without it the operator's resume fails "resume context missing".
332
- // 2. createRun a task_active claim at SUBMIT (+ heartbeat) → reapCtx's task_active guard then
333
- // protects the ctx for the WHOLE first leg. The ctx's updated_at is stamped at submit and
334
- // never refreshed, so a multi-minute first leg (a 5-min gradle build outlived the 120s
335
- // REAP_RUN_STALE_SEC) ages the ctx past the cutoff and reapCtx deletes it MID-RUN — before
336
- // the suspend's pending checkpoint can protect it. setSuspended KEEPS the claim (session
337
- // stays locked submit→resume); a clean completion releases it via setTerminal.
338
178
  let durableTaskId;
339
179
  let durableHeartbeat;
340
- // R8 (CC parity, interactive rewind on disconnect): the user-message entryId is captured INSIDE
341
- // streamBody, but the rewind anchor must be writable from BOTH the done branch AND the disconnect branch
342
- // (Ctrl+C before done) — so hoist it + a once-guarded put to this scope. Without this, a turn cancelled
343
- // before `done` left NO anchor → the shell, even holding the early taskId, 404'd on rewind.
344
180
  let userMsgEntryId;
345
181
  let anchorPut = false;
346
182
  const putRewindAnchor = () => {
@@ -351,68 +187,24 @@ async function handleTasksBody(req, res, url, ctx, miss) {
351
187
  .put(prepared.spec.sessionId, durableTaskId, userMsgEntryId, principal ?? null)
352
188
  .catch(() => deps.metrics?.inc("resume_anchor_capture_failed"));
353
189
  };
354
- // R8 (CC parity): the run-row claim + `durableTaskId` (the REWIND anchor key + the unique
355
- // result.taskId re-stamp) is gated on `runStore` ALONE; the durable-suspend resume ctx (`putCtx`) on
356
- // `checkpointStore`. They were bundled under ONE checkpointStore gate, over-coupling REWIND to
357
- // DURABLE_APPROVAL — but E18 resume-at needs NO checkpoint (main.ts:815). Now matches the /v1/runs bg path:
358
- // rewind works with just runStore (DB_BACKEND=mysql|pg|local), no approval. Claim-first preserved (session locked
359
- // submit→resume; a concurrent submit → 409, never a silent 2nd leg, and a rejected submit never clobbers the ctx).
360
190
  if (deps.runStore && prepared.spec.sessionId) {
361
- const tid = earlyDurableTid ?? uuidv7(); // reuse the id already emitted as the X-Task-Id header (defensive mint if absent)
191
+ const tid = earlyDurableTid ?? uuidv7();
362
192
  const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
363
193
  if (created.ok)
364
- clearTurnActivity(tid); // #245 S1(codex S1-F2):清同 taskId 旧世代活性残留(runs.ts 同款注)
194
+ clearTurnActivity(tid);
365
195
  if (created.ok)
366
- deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget; in-titler dedupe;model 跟 turn([1992]②)
196
+ deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
367
197
  if (!created.ok) {
368
- const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId); // [2255]① 真出路材料
369
- // Headers are already SSE — encode the rejection as the stream's terminal event; the 409 in
370
- // the returned resp is for a deduplicated concurrent caller (and is never idem-cached).
371
- // done 帧 additive 携带同一份材料 —— 形状铸在 toDoneFrameResult(有名字有类型),不在这行内联挑键。
198
+ const conflict = await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId);
372
199
  sseData(res, { type: "done", result: toDoneFrameResult(conflict) });
373
200
  return { status: 409, body: conflict };
374
201
  }
375
202
  durableTaskId = tid;
376
203
  }
377
- // ── stoppedBy 归因(#168 件3:注册点必须晚于 run 认领)────────────────────────────────────
378
- // probe-stoppedby: a client DISCONNECT on the sync stream lane is a USER stop too — Ctrl+C on the
379
- // shell drops the SSE and this abort kills the run, but the mark was only wired on the
380
- // cancel-verb/resume legs, so background children reaped by THIS abort read "parent"/"system"
381
- // instead of "user". Same registration discipline as runs.ts (FIRST on the signal, before core's
382
- // teardown listeners). Owner = the leg's core-canonical taskId(`spec.taskId ?? sessionId`——[3806] 起
383
- // 本腿设 spec.taskId=durableTaskId,canonical 随之;无 durable 行的部署仍回落 sessionId,codex F4 连带);
384
- // skipSessionScoped inside the helper keeps CC Backgrounded children unmarked.
385
- //
386
- // 🔴 **位置即正确性**(#168 件3,3c codex R3 定性的先存病):这条规则说的是「本请求断连 ⇒ 把
387
- // owner=sessionId 的 task-scoped 子代记成人为停止」,而**只有抢到了这条 session 的请求**才有资格
388
- // 那么说。注册早于 `createRun` 时,一次 pre-start 断连会把**另一条活 run** 的子代标成 user-stopped
389
- // (认领随后 409,本请求根本没代表过这条 session)。窗 = SSE 头/首帧 + 开流 preamble(有界 2s)+
390
- // 一次 DB 往返。认领失败那条路在上面就 return 了 ⇒ 一次都不注册。
391
- // 无 durable 账本(无 runStore / 无 sessionId)的部署没有认领这回事,注册点与从前等价。
392
204
  if (prepared.spec.sessionId)
393
205
  markChildrenStoppedByUserOnAbort(ac.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
394
- // [900]①/[906]③ detach 车道逐事件落账:sync 腿历史上只 createRun+setTerminal(events 账本恒空,
395
- // detach 后壳只能轮询终局)。opt-in 车道把 for-await 的每个事件喂给与 bg 腿(runs.ts)共享的
396
- // LedgerSink(src/trace/ledger-sink.ts —— 单一 switch,落账 type/data 形状与 GET /v1/runs/:id/events
397
- // 的 bg 产出完全一致),detach 后壳换道 events tail 全帧续收。非 detach 车道恒 undefined = 零改动
398
- // (不落账,byte-compat)。reasoning gate 与 bg 腿同一配置源(config.traceThinking / TRACE_THINKING)。
399
- // 本腿账面差异(如实;[1840]§五车后已缩两项):suggestions 不落(本腿不调 stream.suggestions)、
400
- // elicitation/question/approval 帧走 live emitAsk 面不落账、stream-open inbox drain 与 observer 臂
401
- // 的 task_notification 走 wire/durable-inbox 既有链(post-detach 自动 park 进 inbox,下一腿 drain
402
- // 补投)——不在本账本。已对齐:model_usage(E8 三件,下方 register/turn_end delta/终局 attach)、
403
- // file_link(live+durable 双写,emit 回调处)。
404
206
  let ledgerSink;
405
- // [900]①: a terminal/park event already landed in the ledger — the failure backstops below must
406
- // never write `failed` over it (bg 腿 `reached` 同款纪律).
407
207
  let ledgerTerminal = false;
408
- // model_usage 双腿对齐([1840]§五,W9 同根):detach 账本腿补上 bg 腿的 E8 三件(register /
409
- // turn_end delta 落账 / 终局 SUM 进 stats.modelUsage)。归因键=**durableTaskId**(codex F4,
410
- // 2026-08-13):core 的 taskId mint = `spec.taskId ?? sessionId`,而 [3806] 起本腿把 durableTaskId
411
- // 塞进了 spec.taskId ⇒ tracer 的 brain.call e.taskId = durableTaskId——usageKey 若还钉 sessionId,
412
- // tracker 的 register-gate 会把整条腿的用量当未注册 no-op(detach 腿 model_usage 帧全丢)。归因键
413
- // 与账本键自此同源(此前「两域分家」的分家理由正是 spec.taskId 缺席,随 [3806] 一并消失)。
414
- // 非 detach 车道照旧不登记(下方 E8 INTENTIONAL 排除注释仍准确——那腿无账本消费者)。
415
- // (usageKey 声明在外层——leak-fence clear 在外层 finally,须覆盖 throw 路径。)
416
208
  if (detachOnDisconnect && durableTaskId && deps.runStore) {
417
209
  const rs = deps.runStore;
418
210
  const tid = durableTaskId;
@@ -422,20 +214,16 @@ async function handleTasksBody(req, res, url, ctx, miss) {
422
214
  deps.modelUsage?.register(usageKey);
423
215
  }
424
216
  if (deps.checkpointStore && prepared.spec.sessionId) {
425
- // putCtx AFTER the claim (resumable). If this throws, the claim row self-heals: heartbeat hasn't
426
- // started, so reapStale flips it failed + releases task_active after runStaleSec.
427
217
  await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
428
218
  }
429
219
  if (durableTaskId && deps.runStore) {
430
220
  const rs = deps.runStore;
431
221
  const tid = durableTaskId;
432
- const owner = principal ?? null; // run created with this owner — pass it as the single-DB null-safe heartbeat guard
222
+ const owner = principal ?? null;
433
223
  const abortFromVerb = () => {
434
- cancelledViaVerb.add(tid); // H2: label the settle (errorCode "cancelled"), see the set's decl
224
+ cancelledViaVerb.add(tid);
435
225
  ac.abort();
436
226
  };
437
- // [1.207 codex H1] heartbeat ALSO polls the durable cancel flag (bg-leg parity, runs.ts) — a
438
- // cancel landing on ANOTHER replica only sets the flag, and this leg previously had no consumer.
439
227
  durableHeartbeat = setInterval(() => {
440
228
  void rs.heartbeat(tid, owner).catch(() => undefined);
441
229
  if (!ac.signal.aborted) {
@@ -444,36 +232,14 @@ async function handleTasksBody(req, res, url, ctx, miss) {
444
232
  }
445
233
  }, HEARTBEAT_MS);
446
234
  durableHeartbeat.unref?.();
447
- // [888]① interactive live-run cancel: register this leg's abort controller under the SAME
448
- // in-flight map the bg/resume legs use — POST /v1/runs/:id/cancel's fast path (`inflightRuns
449
- // .get(taskId)?.abort()`) previously only ever hit bg legs, so cancelling an interactive run
450
- // flipped the durable flag that NOTHING on this leg consumed (release deferred to reapStale).
451
- // Abort rides the leg's existing disconnect-kill chain (spec.signal), which settles the row.
452
235
  inflightRuns.set(tid, ac);
453
- // [1.207 codex H1] registration-window catch-up: a cancel that landed between createRun and this
454
- // point (the awaited putCtx window) flipped the flag while the fast path found no entry — check
455
- // once so it is honored now, not at the first heartbeat.
456
236
  void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !ac.signal.aborted)
457
237
  abortFromVerb(); }).catch(() => undefined);
458
238
  }
459
- // MF-Fleet: publish this run's row transitions to the fleet bus (a tracked/durable run becomes a
460
- // fleet row; turn_end accrues tokens; core `task_progress` adds subagent child rows; terminal removes
461
- // it). No-op for an ephemeral sync run (no durableTaskId) or when no fleetBus is wired.
462
- // [1.211 codex L] fleet 装配纳入清理保护:heartbeat/inflightRuns 注册在上面已发生,而外层
463
- // try/finally 从更后面才开始——fleetRunPublisher/onStart 若在这窗内 throw,计时器与 cancel 句柄
464
- // 无人清理(SSE 头已发,请求以 500 帧终止但资源漏)。let+窄 try/catch 清理后 rethrow 补上这段。
465
239
  let fleetPub;
466
240
  try {
467
241
  fleetPub = durableTaskId
468
- // core 1.154 nested-subagent tree: rootTaskId = the run's CANONICAL core taskId (`spec.taskId ?? sessionId`).
469
- // [3806] 起本腿设 spec.taskId = durableTaskId ⇒ canonical == durableTaskId(codex F4 连带):rootTaskId
470
- // 若还钉 sessionId,core forward 的子代 parentTaskId(=canonical)嵌不进这棵树——fleet 树上成孤儿行。
471
- // 🔒 Scope the fleet row with the SAME identity GET /v1/fleet/stream FILTERS by — `gatedPrincipal`
472
- // (crypto-verified on a direct door), NOT the cost-attribution `principal` (= the spoofable header).
473
- // On a direct door the two diverge: a spoofed header would tag the row under a victim's scope (cross-
474
- // tenant leak) or under "default" while the verified owner's stream filters it OUT (the row vanishes
475
- // from its real owner's view). Tenant-isolation parity with the stream filter (TOC review #6).
476
- ? fleetRunPublisher(deps.fleetBus, { runId: durableTaskId, scope: gatedPrincipal(req, deps.config) ?? "default", rootTaskId: durableTaskId, ...fleetRunLabels(prepared.spec.objective) }) // BC-1: name = short objective preview (description = live-activity, set by the publisher onEvent tool_start)
242
+ ? fleetRunPublisher(deps.fleetBus, { runId: durableTaskId, scope: gatedPrincipal(req, deps.config) ?? "default", rootTaskId: durableTaskId, ...fleetRunLabels(prepared.spec.objective) })
477
243
  : undefined;
478
244
  }
479
245
  catch (e) {
@@ -481,59 +247,34 @@ async function handleTasksBody(req, res, url, ctx, miss) {
481
247
  clearInterval(durableHeartbeat);
482
248
  if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
483
249
  inflightRuns.delete(durableTaskId);
484
- cancelledViaVerb.delete(durableTaskId); // [1.211 codex L] 标记集同窗清理
250
+ cancelledViaVerb.delete(durableTaskId);
485
251
  }
486
252
  throw e;
487
253
  }
488
- // C (TOC adversarial review): settle the fleet row EXACTLY once. A plain Error thrown mid-stream
489
- // (not done/CheckpointError/cancel) escapes the loop before any onTerminal fires → the row leaks as a
490
- // stale "running" forever. Route every onTerminal through this guard so the finally below can settle a
491
- // row no terminal branch reached, without ever double-settling one that did.
492
254
  let fleetSettled = false;
493
255
  const settleFleet = (status, residuals) => {
494
256
  if (fleetSettled)
495
257
  return;
496
258
  fleetSettled = true;
497
- fleetPub?.onTerminal(status, residuals); // [1839]①/(a):真终态帧带残局键(publisher 侧对 parked 再挡一层)
259
+ fleetPub?.onTerminal(status, residuals);
498
260
  };
499
- // 1.207: THIS leg's live TaskStream (assigned inside streamBody) — held here so the outer finally can
500
- // identity-guard its deregistration (only delete the handle WE registered; see the finally).
501
261
  let liveStreamRef;
502
- // design/122 ③: this leg's subagent-handle unregister thunks — run at leg end (finally), not on settle.
503
262
  const subagentHandleEvictions = [];
504
- // false once this leg settles — a bg subagent/bash completion after that has no live
505
- // harness (idle) → the observer parks it in the durable inbox (see fwdInternals below).
506
263
  let syncLegLive = true;
507
- // option A: taskNotificationStreamKey (域:task_id:status[#seq], core [784]④a + codex F2 域二分)
508
- // keys this leg already delivered (turn-open inbox drain, stream branch OR observer) — each arm
509
- // skips keys another arm already carried, so a notification isn't double-written to the SSE (the
510
- // turn-TAIL reap notification, which never reaches the stream, is the one the observer catches;
511
- // core's pend-drain replay of a double-parked idle arrival is the one the drain-registration
512
- // catches — core [784]④b). codex F1: NotifiedKeys (Map keyed on write outcome) for three-leg
513
- // parity — this leg's writes are all synchronous res.write (no async failure window), so every
514
- // claim registers resolved(true) and the has/skip semantics are unchanged here.
515
264
  const syncNotifiedKeys = new NotifiedKeys();
516
265
  try {
517
- fleetPub?.onStart(); // [1.211 codex L] 同窗保护:subscriber throw 不再漏计时器/句柄
266
+ fleetPub?.onStart();
518
267
  }
519
268
  catch (e) {
520
269
  if (durableHeartbeat)
521
270
  clearInterval(durableHeartbeat);
522
271
  if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
523
272
  inflightRuns.delete(durableTaskId);
524
- cancelledViaVerb.delete(durableTaskId); // [1.211 codex L] 标记集同窗清理
273
+ cancelledViaVerb.delete(durableTaskId);
525
274
  }
526
- settleFleet("failed"); // [1.211 codex M2] publishTask 先写行再 emit——subscriber throw 时行已在,不清即永久 running 幽灵行
275
+ settleFleet("failed");
527
276
  throw e;
528
277
  }
529
- // #310 `engine_notice` wire 腿(设计稿 = 黑板 [4630],三腿之一:**sync stream**)。
530
- // core 的 `RunnerDeps.onNotice` 是 deps 级单 sink,通告上没有 runId ⇒ 归属只能靠 `detail.sessionId`;
531
- // 分流器(`createEngineNoticeForwarder`)按会话把白名单三码投到这里注册的口。两口分开注册:
532
- // · live = 本连接的 SSE(断流按既有 fail-open 词表留痕,不静默丢);
533
- // · durable = detach 车道的账本(非 detach 车道 ledgerSink 恒 undefined ⇒ 无 durable 口,与本腿
534
- // 其它帧的账面姿态一致;断连补看的那半由 detach 车道 / bg 腿承担)。
535
- // 账本写不 flush 缓冲文本 —— 与 `onForwardEvent` 同款 fire-and-forget 姿态(seq 在链步内同步分配,
536
- // 不会与主循环的 await 写乱序);代价是通告行可能排在同一 turn 的 text 行之前,那是"带外行"的既有账面语义。
537
278
  const noticeLedger = ledgerSink;
538
279
  const unregisterEngineNotice = registerEngineNoticeLeg({
539
280
  sessionId: prepared.spec.sessionId,
@@ -544,44 +285,23 @@ async function handleTasksBody(req, res, url, ctx, miss) {
544
285
  }
545
286
  sseData(res, { type: "engine_notice", ...row });
546
287
  },
547
- // 写失败留痕不裸吞(codex 对抗复审 R1-[medium]):`route` 的同步 try 观察不到异步 reject,
548
- // 本腿丢的是**断连补看**那一份(live SSE 已投),仍必须让"系统性在漏"在遥测里显形。
549
288
  ...(noticeLedger
550
289
  ? {
551
290
  durable: (row) => void noticeLedger.append("engine_notice", row).catch(() => recordFailOpen("server.engine-notice.durable-append-failed", `leg=sync code=${row.code} task=${durableTaskId ?? ""}`)),
552
291
  }
553
292
  : {}),
554
293
  });
555
- // R8 rewind handle is now the X-Task-Id RESPONSE HEADER (set with earlyDurableTid above), not an
556
- // out-of-contract `run_started` data frame — the shell reads it from the response headers even on an early
557
- // disconnect, and the SDK's CLOSED AgentEvent stream stays unpolluted (workflow re-review #2 caught the drift).
558
294
  try {
559
295
  const streamBody = async () => {
560
296
  await withPrincipal(principal, async () => {
561
- // core 1.154 nested-subagent live tree: opt into the forward sink — a subagent's task_progress
562
- // lives in its ISOLATED child stream (never on THIS top stream; core's only emitter is
563
- // parentToolCallId-gated), so this sink is the ONLY path a tick reaches the top-run consumers.
564
- // 1.78 (review HIGH): fan it out to the LIVE SSE too (shared whitelist+redact builder, carrying
565
- // the E2 identity the shell binds concurrent ticks by) — previously it fed ONLY the fleet bus,
566
- // so the conversation lane never saw a real tick and the in-loop task_progress branch below was
567
- // forward-compat then — [2373]B-4:floor ^4.0.0 下它是热路径(腐注方向已翻转,勿按旧注当死支)。
568
- // core swallow-guards a throwing sink.
569
- // [3911] 定谳(bg 腿 twin 在 runs.ts):per-agent tail 的**内容帧供给**由本腿 spec 的
570
- // forwardSubagentEvents 决定(core prepare-task 的 forward 门);缺席 ⇒ 那条流恒只有
571
- // task_progress。逐帧按**句柄**登记供给形(键为什么是句柄:SubagentTailBus.modeByHandle 顶注)。
572
297
  const tailContentMode = prepared.spec.forwardSubagentEvents === true ? "on" : "progress_only";
573
298
  const fwdInternals = {
574
299
  onForwardEvent: (e) => {
575
300
  fleetPub?.onForwardEvent(e);
576
- // [900]① detach 车道:子代理 forward 事件(tool_start/end + task_progress)也落账 —— bg 腿
577
- // 同姿势(共享 sink 的 fire-and-forget 门,child deltas 有意不逐块落,见 LedgerSink.onForwardEvent)。
578
301
  ledgerSink?.onForwardEvent(e);
579
- // S2 live tail(core 1.370 bgAgentId;bg 腿 twin 在 runs.ts):句柄分发在 res 存活判之前
580
- // ——主流断了 tail 订阅者照收(tail 是独立连接)。
581
302
  {
582
303
  const bg = e.bgAgentId;
583
304
  if (bg !== undefined) {
584
- // 登记在 hasSubscribers 快门**之前**:没人在听时也要学,否则晚开的 tail 读不到供给形。
585
305
  defaultSubagentTailBus.noteHandleContentMode(bg, tailContentMode);
586
306
  if (defaultSubagentTailBus.hasSubscribers(bg)) {
587
307
  const f = projectTailFrame(e);
@@ -597,78 +317,37 @@ async function handleTasksBody(req, res, url, ctx, miss) {
597
317
  sseData(res, { type: "task_progress", ...taskProgressEventData(e) });
598
318
  }
599
319
  else if (t === "tool_start") {
600
- // C1 (core 1.219, spec.forwardSubagentEvents): a delegated child's live CONTENT events now
601
- // ride the forward sink (each stamped parentToolCallId — the shell nests them under the
602
- // delegation card). Echo through the SAME shared whitelist+redact builders as the top
603
- // stream (§E1 — forwarded events are UNTRUSTED RAW; redaction is ours at the wire).
604
320
  const tsv = e;
605
321
  sseData(res, { type: "tool_start", ...toolStartEventData(tsv) });
606
322
  }
607
323
  else if (t === "tool_end") {
608
324
  const te = e;
609
- sseData(res, { type: "tool_end", ...toolEndEventData(te) }); // label 现由 builder 自带,外拼冗余已清
325
+ sseData(res, { type: "tool_end", ...toolEndEventData(te) });
610
326
  }
611
327
  else if (t === "text_delta") {
612
- // Child text is plain model output (same trust class as the top stream's text_delta, which
613
- // rides verbatim); parentToolCallId attributes it. Whitelist the three fields explicitly.
614
328
  const td = e;
615
329
  sseData(res, { type: "text_delta", delta: td.delta, ...(td.eventId ? { eventId: td.eventId } : {}), ...(td.parentToolCallId ? { parentToolCallId: td.parentToolCallId } : {}) });
616
330
  }
617
331
  else if (t === "reasoning_delta") {
618
- // §E1 parity with the top stream's live CoT: redactSecrets before echo.
619
332
  const rd = e;
620
333
  sseData(res, { type: "reasoning_delta", delta: redactSecrets(rd.delta), ...(rd.eventId ? { eventId: rd.eventId } : {}), ...(rd.parentToolCallId ? { parentToolCallId: rd.parentToolCallId } : {}) });
621
334
  }
622
335
  },
623
- // background-completion observer — flip the child's fleet
624
- // row (task_progress never emits a terminal tick) + park an idle-arrival in the durable
625
- // inbox (workflow-type skipped: own durable chain). Rationale in runs.ts (bg-leg twin).
626
- // core 1.283: `seq` = the per-stop-cycle counter (stop→wake→stop mints 1,2,…) — it MUST
627
- // ride into the dedup key below (core [784]④a) and into the parked inbox entry.
628
- // 白名单排查(2026-07-24,「彻查 as {} 连环坑」):手写窄类型换 core 真实类型,理由同 runs.ts twin。
629
336
  onTaskNotification: (n) => {
630
337
  if (n.task_type === "workflow")
631
338
  return;
632
- // S2 复审 #3:中途终态信号——task_settled 帧让订阅者不再 heartbeat 永挂(路由收帧即收流)。
633
- // [3911] 终态即撤供给形登记:同一 a* 句柄可进入下一个**复活周期**,而新周期的宿主腿模式
634
- // 可能相反——不撤会把上一周期的答案报给新周期的首帧前窗口(理由全文见 forgetHandleContentMode)。
635
339
  defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
636
340
  if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
637
341
  defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
638
342
  }
639
- // id-domain alias: flip by payload.sessionId (= the tick's uuid domain), fallback task_id.
640
- // [2687-cli] 幽灵行案:只对 agent 族终态打(isFleetAgentTerminalNotification 单源判别,
641
- // 病灶链见其 doc 注);bash/monitor 的通知帧/park 面照走。孪生:runs.ts bg 腿、http/server.ts resume 腿。
642
343
  const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
643
- // 🔴 对抗评审 2026-07-11(HIGH,「下一 turn 0 帧」最强根因):the teardown WINDOW — after the
644
- // client disconnects (the user killed the turn → ac.abort → core reaps the bg child → THIS
645
- // notification fires) but BEFORE the outer finally flips `syncLegLive=false` (several awaits sit
646
- // between the for-await break and the finally). In that window the old predicate neither parked
647
- // (legLive still true) nor wrote (socket dead) — the notification vanished with no inbox copy and
648
- // no frame. Fold the dead-socket state into the park predicate: the leg is only DELIVERABLE when
649
- // it is live AND the socket can still be written. 🔴 `res.destroyed` is the CLIENT-disconnect
650
- // signal (`writableEnded` only flips when WE call res.end()) — the real-HTTP retest caught the
651
- // writableEnded-only predicate still writing into the dead socket.
652
344
  const deliverable = syncLegLive && !res.writableEnded && !res.destroyed;
653
345
  const parked = !deliverable && Boolean(deps.workflowCompletionInbox && prepared.spec.sessionId);
654
- // diagnosability (rationale in runs.ts twin).
655
346
  deps.logger?.info?.("task_notification_observed", { route: "sync-stream", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: syncLegLive, parkedDurable: parked });
656
347
  if (parked) {
657
- // 对抗评审 2026-07-11(MED):the fire-and-forget swallow hid EVERY enqueue failure (SQL blip,
658
- // fence race) while the diag line above had already printed parkedDurable:true — a false park✓
659
- // fingerprint. Log the failure; behavior (never fault the callback) unchanged.
660
348
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(prepared.spec.sessionId, askOwner, n, Date.now(), durableTaskId)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "sync-stream", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
661
349
  }
662
350
  else if (deliverable) {
663
- // option A (turn-TAIL killed receipt must be IMMEDIATE): a teardown-time reap
664
- // notification fires while the SSE is still open but past the stream's delivery window —
665
- // write the display frame directly, skipping keys the stream already carried (mid-turn
666
- // dedup; the shell's dedup stays the outer belt). core [784]④a: the key folds `seq` in
667
- // (shared taskNotificationStreamKey) — the old seq-less `${task_id}:${status}` shape
668
- // REVERSE-swallowed a second stop-cycle's same-status frame on this arm (stop→wake→stop:
669
- // the seq=2 completed frame collided with cycle 1's key and the user never saw it).
670
- // codex F1: res.write is a synchronous enqueue (no async failure window — a dead socket
671
- // is already folded into `deliverable` above), so the claim registers resolved(true).
672
351
  const key = taskNotificationStreamKey(n);
673
352
  if (syncNotifiedKeys.get(key) === undefined) {
674
353
  syncNotifiedKeys.set(key, Promise.resolve(true));
@@ -676,10 +355,6 @@ async function handleTasksBody(req, res, url, ctx, miss) {
676
355
  }
677
356
  }
678
357
  },
679
- // C2 (core 1.219) + design/122 (core 1.225 ③): register each sync delegation's steer handle
680
- // under this run's durable taskId (the X-Task-Id the shell holds). NOT evicted on settle —
681
- // resume is only legal after settle; evicted at leg end via the collected identity-guarded
682
- // thunks (outer finally), so a settled child stays steer-409/resume-able while the run lives.
683
358
  ...(deps.subagentSteerRegistry && durableTaskId
684
359
  ? {
685
360
  onSubagentSpawn: (handle) => {
@@ -688,16 +363,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
688
363
  }
689
364
  : {}),
690
365
  };
691
- // R8 (CC parity, interactive rewind): the sync `tasks.stream` path (the REPL's lane) must ALSO
692
- // leave a per-prompt rewind anchor, else an interactive-turn rewind 404s (the bg + resume legs capture
693
- // it, this one didn't). Buffer the committed USER-message entryId on the HOISTED `userMsgEntryId` (so
694
- // the disconnect branch can anchor too); the done branch anchors here, the disconnect branch below.
695
- // core 1.207: hold the LIVE TaskStream handle + register it (keyed by durableTaskId = the
696
- // X-Task-Id the shell already holds) so the replica-local verbs — POST /v1/runs/:id/detach
697
- // (mid-flight ctrl+b) and, as a natural consequence, steer/compact — now reach a mid-turn
698
- // INTERACTIVE run too (previously only the bg + resume legs registered). Deregistered in the
699
- // outer finally (with the fleet settle), so an early throw can't leak a stale handle.
700
- const liveStream = prepared.runner.runTaskStream({ ...prepared.spec, signal: ac.signal, ...(durableTaskId ? { taskId: durableTaskId } : {}) }, undefined, fwdInternals); // [3806]:tid 必须进 spec.taskId——缺席时 core 按 sessionId fallback 喂 noteTaskRun,lastRunId 投影被污染(bg 腿一直有传,两腿对称) // #196:场景 hands 判别位选出的 Runner
366
+ const liveStream = prepared.runner.runTaskStream({ ...prepared.spec, signal: ac.signal, ...(durableTaskId ? { taskId: durableTaskId } : {}) }, undefined, fwdInternals);
701
367
  liveStreamRef = liveStream;
702
368
  if (durableTaskId)
703
369
  steerableRuns.set(durableTaskId, liveStream);
@@ -707,47 +373,19 @@ async function handleTasksBody(req, res, url, ctx, miss) {
707
373
  if (ev.type === "message_committed" && ev.role === "user")
708
374
  userMsgEntryId = ev.entryId;
709
375
  if (ev.type === "done") {
710
- // Never stream the durable-resume capability token to the client: a
711
- // durable suspend's done event carries it. Strip it from BOTH the live write and the cached
712
- // body (handed to a concurrent deduplicated caller below). No-op for a normal result.
713
- // [4913]:graft 先于 strip——durable park 的 done 帧顶层带待批 toolCallId(cli 判别子 v3
714
- // 写者② 的读点;tool-less park / 非 park / 读失败 = 键缺席,strip 语义不变)。
715
376
  finalResult = stripCheckpointToken(await graftParkToolCallId(ev.result, deps.checkpointStore));
716
- // R8 (CC parity, interactive rewind): on the sync leg core sets result.taskId = sessionId
717
- // (spec.taskId is left unset so a forwarded subagent nests under the run — server.ts above), which
718
- // is NOT unique per prompt → it can't be a per-turn rewind handle (every turn would collide). Re-
719
- // stamp the WIRE result.taskId to the unique durable run id (uuidv7, == the bg path's result.taskId
720
- // → consistent across paths) so the shell gets a unique per-prompt handle. Internal canonical
721
- // (fleet/subagent nesting) keeps sessionId — only the external done-frame value changes. Durable-run
722
- // only (ephemeral has no anchor store ⇒ no rewind anyway), so the fallback keeps sessionId.
723
377
  if (durableTaskId && finalResult)
724
378
  finalResult = { ...finalResult, taskId: durableTaskId };
725
- // [1.207 codex H2] cancel-verb abort label: core's external-signal abort yields
726
- // failed WITHOUT an errorCode — project `cancelled` onto the wire+persisted form so
727
- // /cancel's outcome is uniform (first-writer-wins would pin the unlabeled form). Only
728
- // an abort-shaped FAILED result is labeled — a completion that raced the abort stands.
729
379
  if (durableTaskId && cancelledViaVerb.has(durableTaskId) && finalResult?.status === "failed" && !finalResult.errorCode) {
730
380
  finalResult = { ...finalResult, errorCode: "cancelled" };
731
381
  }
732
382
  finalizeTaskResult(ev.result, principal, prepared.spec.objective, prepared.spec.sessionId);
733
- // Anchor this interactive prompt (taskId → user-message entryId) keyed by the SAME unique id the
734
- // shell now holds (done.result.taskId == durableTaskId). entryId came straight from
735
- // `message_committed{role:"user"}` (no getLeafId). Best-effort + once-guarded — a capture failure
736
- // only makes THIS turn non-rewindable, never faults the run; the disconnect branch shares the guard.
737
383
  putRewindAnchor();
738
- settleFleet(finalResult?.status ?? "completed", fleetRunResiduals(finalResult)); // [1839]①/(a):终态残局键
739
- // [900]① detach 车道:done 也落账(flush 攒批 + suspended/needs_review/done 投影,bg 词汇;
740
- // finalResult 已 stripCheckpointToken + taskId 重印 + cancel 标注 —— 账本形 == wire 形)。
741
- // 放在 res.write 之前:壳收到 wire done 后立刻换道 events tail 也必见终局帧。既有
742
- // finalize/settleFleet/res.write/setTerminal 顺序不动(落账是旁路)。
384
+ settleFleet(finalResult?.status ?? "completed", fleetRunResiduals(finalResult));
743
385
  if (ledgerSink && finalResult) {
744
- // E8 双腿对齐:终局 flush 最后一拍 delta + SUM 全账本 model_usage 进 stats.modelUsage
745
- // (bg 腿 withModelUsage 同形)。键自 codex F4 修后同源:usageKey=durableTaskId=账本键
746
- // (getEvents 包一层保留——按变量各取所需,同值不同职,防未来再分家时又要拆)。
747
386
  if (usageKey && deps.modelUsage && deps.runStore && durableTaskId) {
748
387
  const rs2 = deps.runStore;
749
388
  const tid2 = durableTaskId;
750
- // attach 失败(SUM 读瞬断)不许炸掉 onDone——终局帧比 modelUsage 汇总重要,回落原 result。
751
389
  const fr = finalResult;
752
390
  finalResult = await attachModelUsage(fr, { append: ledgerSink.append, getEvents: (_id, after) => rs2.getEvents(tid2, after), modelUsage: deps.modelUsage, taskId: usageKey }).catch(() => fr);
753
391
  }
@@ -757,119 +395,57 @@ async function handleTasksBody(req, res, url, ctx, miss) {
757
395
  sseData(res, { ...ev, result: finalResult });
758
396
  continue;
759
397
  }
760
- // [900]① detach 车道逐事件落账(await 保序 —— 与 bg 腿同代价姿势;message_committed/未知型
761
- // sink 内部 no-op)。非 detach 车道 ledgerSink 恒 undefined = 零改动。
762
398
  if (ledgerSink) {
763
399
  await ledgerSink.onEvent(ev);
764
- // E8 双腿对齐:turn 边界 drain per-model delta 落账(bg 腿 flushModelUsage 同位同形)。
765
400
  if (ev.type === "turn_end" && usageKey)
766
401
  await appendModelUsageDelta(ledgerSink.append, deps.modelUsage, usageKey);
767
402
  }
768
- fleetPub?.onEvent(ev); // MF-Fleet: turn_end → tokens; task_progress → subagent child row
769
- // S4 P0-a: count brain-liveness phases on the sync leg too. The bg/resume legs count at
770
- // their append sites.
403
+ fleetPub?.onEvent(ev);
771
404
  if (ev.type === "status")
772
405
  deps.metrics?.inc("brain_retry_total", { phase: String(ev.phase) });
773
- // E1 (shell-host contract): tool_end.output AND (core 1.203) tool_end.structured are
774
- // UNTRUSTED RAW from core (file/shell/API bodies; a structured card carries `originalFile`) —
775
- // redact at the live wire boundary before echo (core only SIZE-bounds; redaction is ours, same
776
- // contract as the durable path). 🔒 EXPLICIT WHITELIST, not `{...ev}` (the 1.72 task_progress
777
- // lesson, re-proven by `structured`: a spread forwards a future core field RAW — 1.203 landing
778
- // meant `structured` rode this leg unredacted until this build). Known-safe fields verbatim:
779
- // ids/name/label/isError/truncated + E2 identity. NB the old guard also skipped redaction when
780
- // `output` was undefined (→ raw catch-all) — the whitelist path now covers every tool_end.
781
406
  if (ev.type === "status") {
782
- // TaskEvent 三腿审查(steering_injected 跟进批):`status`(BrainStatus.detail,UNTRUSTED —
783
- // 可能回显 provider 的 rate-limit/retry 报文)此前落裸 catch-all 未脱敏,与 durable 两腿
784
- // (ledger-sink.onEvent + resume switch)共用的 brainStatusEventData 脱敏不一致——同类
785
- // 「留在 catch-all」教训(1.72 task_progress),现挪显式分支,三腿同门同形。
786
407
  sseData(res, { type: "status", ...brainStatusEventData(ev) });
787
408
  }
788
409
  else if (ev.type === "tool_end") {
789
410
  const te = ev;
790
- sseData(res, { type: "tool_end", ...toolEndEventData(te) }); // label 现由 builder 自带,外拼冗余已清
411
+ sseData(res, { type: "tool_end", ...toolEndEventData(te) });
791
412
  }
792
413
  else if (ev.type === "tool_start") {
793
- // 🔒 E1 parity (TOC adversarial review): redact tool_start.args on the LIVE wire too — the model's
794
- // tool-call inputs can echo a secret it read (e.g. a value from a redacted tool_end fed into the
795
- // next call). Whitelist for the same reason as tool_end (shared durable builder = one shape).
796
414
  const tsv = ev;
797
415
  sseData(res, { type: "tool_start", ...toolStartEventData(tsv) });
798
416
  }
799
417
  else if (ev.type === "task_notification") {
800
- // core 1.202 (design/115 P2): a background task (workflow) finished while THIS harness was live —
801
- // core already injected the <task-notification> XML into the model in-process; this is the display
802
- // projection for the shell. Whitelist + redact via the shared builder (summary/result are UNTRUSTED
803
- // free text). The shell dedups against the inbox-drain `workflow_complete` lane by run id.
804
- // core [784]④b(跨层双泊车): a notification that arrived while the previous leg was idle is
805
- // parked TWICE — core pends it in-Runner (pendingSessionNotifications) AND our sync observer
806
- // parks it in the durable inbox. On this turn-open, our inbox drain (below the streamBody def)
807
- // already wrote the frame and REGISTERED its key; core's pend-drain replays the same payload as
808
- // a stream `task_notification` event (runtask.js queue.push) — skip it here instead of writing
809
- // the wire twice. Same check also folds the mid-turn observer-then-stream echo. A frame whose
810
- // key is NOT yet on this stream writes exactly as before (the first copy is never eaten).
811
- // codex F1/F2: same await-the-claim shape as the bg/resume twins (on THIS leg every claim
812
- // is resolved(true) — sync writes — so the await is immediate); task_type rides the key.
813
418
  const tnv = ev.notification;
814
419
  const tnvKey = tnv?.task_id ? taskNotificationStreamKey({ task_type: tnv.task_type, task_id: tnv.task_id, status: String(tnv.status), seq: tnv.seq }) : undefined;
815
420
  const tnvPrior = tnvKey !== undefined ? syncNotifiedKeys.get(tnvKey) : undefined;
816
421
  if (tnvPrior === undefined || !(await tnvPrior)) {
817
422
  if (tnvKey !== undefined)
818
- syncNotifiedKeys.set(tnvKey, Promise.resolve(true)); // stream delivered this key
423
+ syncNotifiedKeys.set(tnvKey, Promise.resolve(true));
819
424
  sseData(res, { type: "task_notification", ...taskNotificationEventData(ev) });
820
425
  }
821
426
  }
822
427
  else if (ev.type === "reasoning_delta" && typeof ev.delta === "string") {
823
- // 🔒 E1 parity (TOC adversarial review): redact the CoT on the LIVE wire too. The agent's
824
- // reasoning can echo a secret it read (core [R57]: CoT may carry reasoned-about secrets). The
825
- // DURABLE path redacts it (runs.ts → redactSecrets, gated by persistThinking/TRACE_THINKING);
826
- // the live mirror MUST match. Keep STREAMING the (redacted) thinking so live CoT still renders.
827
- // 🔒 EXPLICIT WHITELIST, not `{...ev}` (TaskEvent 三腿审查跟进批, 1.72 task_progress lesson
828
- // re-proven here: a spread forwards an unknown/future field RAW; this arm was the one
829
- // sibling branch that still spread — the FORWARDED twin below [t === "reasoning_delta"]
830
- // already used the narrow form, this now matches it byte-for-byte).
831
428
  sseData(res, { type: "reasoning_delta", delta: redactSecrets(ev.delta), ...(ev.eventId ? { eventId: ev.eventId } : {}), ...(ev.parentToolCallId ? { parentToolCallId: ev.parentToolCallId } : {}) });
832
429
  }
833
430
  else if (ev.type === "task_progress") {
834
- // 🔒 E1 live-wire parity: explicit whitelist via the SHARED builder (UNTRUSTED
835
- // `name` redacted; 1.78: E2 identity rides — the shell binds concurrent subagent ticks by
836
- // eventId/parentToolCallId) — parity with the durable runs.ts + resume legs.
837
431
  sseData(res, { type: "task_progress", ...taskProgressEventData(ev) });
838
432
  }
839
433
  else if (ev.type === "compacted") {
840
- // core 1.219 (attachWorkingFiles default ON): `compacted` now carries `attachedFiles` — route it
841
- // through the SHARED whitelist builder (paths redactSecrets'd) instead of the raw catch-all, so
842
- // the live wire matches the durable legs' shape exactly (one builder, no drift).
843
434
  sseData(res, { type: "compacted", ...compactedEventData(ev) });
844
435
  }
845
436
  else if (ev.type === "diagnostics") {
846
- // core 1.220 (design/121): the shell's render source for CC's diagnostics row.
847
- // Shared whitelist builder (§E1: language-server output UNTRUSTED RAW → redactSecrets).
848
437
  sseData(res, { type: "diagnostics", ...diagnosticsEventData(ev) });
849
438
  }
850
439
  else if (ev.type === "steering_injected") {
851
- // [1550]/[1552]: core's mid-run system-reminder injection — shared whitelist builder
852
- // (§E1: preview is UNTRUSTED free text → redactSecrets). Previously fell through to the
853
- // raw catch-all below (unredacted `{...ev}`) — moved to an explicit builder for the same
854
- // reason every other content type left the catch-all (the 1.72 task_progress lesson).
855
440
  sseData(res, { type: "steering_injected", ...steeringInjectedEventData(ev) });
856
441
  }
857
442
  else if (ev.type === "workspace_changed") {
858
- // [1559]二 core 裁定:bash cd/EnterWorktree 移动任务逻辑 cwd 后一帧,给 shell UI 的路径
859
- // 提示行。cwd 与已展示的工具 args 同信任级 → 共享白名单构造器(redactSecrets + bound)。
860
443
  sseData(res, { type: "workspace_changed", ...workspaceChangedEventData(ev) });
861
444
  }
862
445
  else if (ev.type === "compaction_outcome") {
863
- // core 2.5.0([2105]②)压缩非 compacted 结局帧。[2373]C-2:内联挑键收进共享 builder,
864
- // 与两条 durable 腿同源(它曾是唯一不走 builder 的内容帧=三方普查共同盲区)。
865
446
  sseData(res, { type: "compaction_outcome", ...compactionOutcomeEventData(ev) });
866
447
  }
867
448
  else if (ev.type === "wiring_manifest") {
868
- // 🔴 core 5.14.0 design/173 新臂。`manifest.governance` 盖着 `audience:"operator"`,core 把
869
- // 失败方向写死成「no projection ⇒ do not disclose」——所以默认(租户)形整段剥除,含**对该段
870
- // 取的哈希** `configFingerprint`(无盐 + 4 布尔 = 16 组合谕示器)。
871
- // #154 件一:分叉只在这条 live 腿(**唯一知道调用方身份的腿**),两条 durable 腿恒存剥后形
872
- // ——账本写时不知道未来读者是谁,存全量 = 把 operator-only 面写进租户可读介质。
873
449
  sseData(res, {
874
450
  type: "wiring_manifest",
875
451
  ...(wiringOperatorFace
@@ -878,19 +454,9 @@ async function handleTasksBody(req, res, url, ctx, miss) {
878
454
  });
879
455
  }
880
456
  else if (ev.type === "human_input") {
881
- // core 5.14.0 design/171 新臂:谁把什么喂进了这条 run(本帧不带正文,只带载体/署名)。
882
- // 同上——离开裸 catch-all:`issuer`/`actor.*` 是宿主派生的身份串(自由文本形)⇒ 构造器
883
- // 内过 redactSecrets;`principal` 刻意不投影(帧本就走 owner-scoped 流)。
884
457
  sseData(res, { type: "human_input", ...humanInputEventData(ev) });
885
458
  }
886
459
  else if (ev.type === "text_delta" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
887
- // 裸 catch-all 白名单化收官(probe-wire-1 表征钉的根治半场):最后四个已知臂离开
888
- // `JSON.stringify(ev)`。**零字节变化**白名单——已知键(含 E2 四 identity 键:live 现状
889
- // 带全四键,消费端可能已依赖,与 durable builder 的「刻意排除」是两腿两策)逐一挑,
890
- // 只挡 core 未来新键的裸穿透(structured 在 1.203 就真穿透过本腿)。text_delta 的 delta
891
- // 不做内容变换:逐 token 片段上 redactSecrets 对跨 chunk 的秘密无效还破流式——durable
892
- // 腿攒批后 redact 是那半的正解,两腿口径注在 ledger-sink.flush。catch-all(下方 else)
893
- // 从此只剩**真未知臂**:L1 臂集门(core-keyset-guard ⑤)在 core 加臂时 tsc 红逼显式处置。
894
460
  const e = ev;
895
461
  const ident = {
896
462
  ...(e.eventId !== undefined ? { eventId: e.eventId } : {}),
@@ -913,83 +479,26 @@ async function handleTasksBody(req, res, url, ctx, miss) {
913
479
  }
914
480
  });
915
481
  };
916
- // E23 + §4④: wrap the synchronous stream in the per-run elicitation AND AskUserQuestion contexts (nested
917
- // — each uses its own ALS). emit = a named SSE frame on THIS open connection; taskId keys each throttle
918
- // (the durable run id, else a synthetic internal id); owner gates the respond (the VERIFIED principal —
919
- // NOT the cost-attribution `principal`, which is the spoofable header and null on a direct door → would
920
- // park owner=null and let any tenant answer); ac.signal cancels a parked ask on client disconnect. Each
921
- // wrap is a no-op when its coordinator is off; the two share the identical id/owner/emit/signal.
922
482
  const askTaskId = durableTaskId ?? uuidv7();
923
483
  const askOwner = gatedPrincipal(req, deps.config) ?? null;
924
484
  const emitAsk = (frame) => {
925
- // #157(codex R2 复审 HIGH):流已断时这里是**静默 no-op** —— 上游 coordinator 的 try/catch
926
- // 因此拿不到任何信号,一次断连就被记成"投递成功",该 ask 挂到 TTL 才按无人应答结算。当时
927
- // 方向不动,但**必须留痕**:否则 `open-frame-undelivered` 只覆盖"持久化抛错",覆盖不到真正
928
- // 常见的那条路。
929
- // ⚠️ **question 半场已在 #173 换了方向**(a318fa1):question / question_complete 两型帧改走
930
- // 下方 `emitQuestion` 的断流 THROW,于是它们**到不了**本 no-op 臂(两道守卫同步相邻、其间无
931
- // await);实测 park 墙钟 8002ms → <160ms。今天真正还落在这条静默臂上的只有 elicitation 族
932
- // 与其它运行流帧。别再据本段把 question 的 TTL 悬挂当作现行行为。
933
- // `destroyed` 与 `writableEnded` 一起判——emitApproval 用的就是这对谓词。
934
- // 分类按帧型走(`failOpenTagForDroppedFrame`):这条闭包同时驮着 open 帧、完成面包屑与
935
- // 其它运行流帧,一刀切会让**一次**断连把保护型计数记两次,真 open 失败率被面包屑盖住。
936
485
  if (res.writableEnded || res.destroyed) {
937
486
  recordFailOpen(failOpenTagForDroppedFrame(frame.type), `frame=${frame.type}`);
938
487
  return;
939
488
  }
940
489
  res.write(`event: ${frame.type}\ndata: ${JSON.stringify(frame)}\n\n`);
941
490
  };
942
- // P1 ①②: this new turn is a stream-open for the session — push any async-workflow completions that
943
- // finished since (frames ride the SAME connection, before the run's own events; the shell dedups by
944
- // runId). `askOwner` = the VERIFIED principal → only this caller's own completions are emitted.
945
- // 🔒 THROW on a dead/ended stream instead of emitAsk's silent no-op (1.80 review: a disconnect
946
- // between request-send and this drain made the two-phase ack a FALSE success — the entry was
947
- // removed with no frame ever written; throwing keeps it pending for the next leg). This wrapper was
948
- // once the ONLY one that threw; since #173 (a318fa1) emitQuestion, emitApproval and writeLive all
949
- // throw on a dead stream too. Of the HITL family only ELICIT still rides emitAsk's no-op — but emitAsk
950
- // is NOT elicit-only: SendUserFile's `file_link` (below) and other runtime frames also write straight
951
- // through it, so its dead-stream drop surface is wider than the HITL lanes (file_link is covered by its
952
- // own durable twin, not by a throw).
953
- // 🔴 drain 案: write these as ANONYMOUS `data:` frames, NOT emitAsk's NAMED
954
- // `event: <type>` form — the run's own live-leg task_notification frames on this SAME connection
955
- // are anonymous (L1392/L1474), so a shell parsing the default message channel silently never saw
956
- // a drained frame (the Monitor "park✓ drain✗" fingerprint). The HITL frames keep their own
957
- // established NAMED-event contract — elicit still writes straight through emitAsk, question rides
958
- // emitQuestion (same named-event write, one extra dead-stream guard in front, #173).
959
491
  await emitPendingWorkflowCompletions(deps.workflowCompletionInbox, prepared.spec.sessionId, askOwner, (frame) => {
960
492
  if (closed || res.writableEnded || res.destroyed)
961
493
  throw new Error("stream ended before the completion frame was written");
962
- // core [784]④b: register the drained frame's key on THIS stream — the same idle-arrival was
963
- // ALSO parked core-side (pendingSessionNotifications), and its pend-drain replay lands on this
964
- // very stream as a task_notification event; the loop's key check folds that second wire copy.
965
- // codex F1: resolved(true) — the res.write below is synchronous (the dead-stream guard above
966
- // already threw). codex F2: task_type (restored from the entry extras) rides into the key —
967
- // an external notify's caller-chosen task_id must not pre-claim an internal task's key.
968
494
  const f = frame;
969
495
  if (f.type === "task_notification" && f.task_id)
970
496
  syncNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), Promise.resolve(true));
971
497
  sseData(res, frame);
972
- },
973
- // emit-target diagnostics — this drain rides the caller's OWN live SSE response.
974
- { route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
498
+ }, { route: "sync-stream-open", connection: "live-sse", log: (m, x) => deps.logger?.info?.(m, x) });
975
499
  const withElicit = deps.elicitation
976
500
  ? () => deps.elicitation.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitAsk, abortSignal: ac.signal }, streamBody)
977
501
  : streamBody;
978
- // #152 复审 A1:`deliverable` 是 durable 部署上 park-vs-live 判决的投递面谓词(见 question.ts 该键注)。
979
- // 本腿的投递面就是这条 SSE 连接,而 ①b detach 车道下客户端断连**不 abort、不结束这条腿**——run 继续在
980
- // 本 runWithContext 作用域里跑。谓词让判决面此时如实读作「无活流」⇒ durable park(运维可经 /decide
981
- // 补答),而不是 allow 后挂满 ttl。
982
- // #173([2937] test 墙钟反证定谳):question 开帧的 emit 不再共享 emitAsk 的静默 no-op——死流上
983
- // no-op = 开帧「假投递成功」,coordinator 等满 TTL 才以 unavailable 结算(实测 park 墙钟 ≈TTL,
984
- // 断连→ask 间隔拉到 15s 也一样:判决在 gate 时刻,而 gate 早已判过的窗口里 ask 只剩 emit 一道门)。
985
- // 收紧与 emitApproval 同形:断流 THROW ⇒ #166 的 delivery==="failed" 臂**立即** settle
986
- // unavailable,不再等满 TTL。
987
- // ⚠️ 收紧买到的是**结算速度**,不是可恢复性(2026-08-07 亲读 core 5.16 定):本闭包只在工具已
988
- // 执行时被调用,即 `createDurableQuestionGate` 已判 allow、core 已过 `suspendAsk` 挂起点 ⇒ 这一
989
- // 形落 `declined_unavailable` 自答续跑卡(赎回既有审批的腿返 isError),**不铸 checkpoint**。
990
- // 真正 park 的是判决时投递面就不可达的腿(`deliverable`/`hasLiveContext` 读 false ⇒ 门判 ask),
991
- // 它们根本走不到这里。别把两形写成一句。
992
- // elicitation 保持 no-op(其 decline 语义 fail-closed,#166 车登记另议)。
993
502
  const emitQuestion = (frame) => {
994
503
  if (res.writableEnded || res.destroyed)
995
504
  throw new Error("live stream ended — question frame undeliverable");
@@ -998,27 +507,11 @@ async function handleTasksBody(req, res, url, ctx, miss) {
998
507
  const withQuestion = () => deps.question
999
508
  ? deps.question.runWithContext({ taskId: askTaskId, owner: askOwner, emit: emitQuestion, abortSignal: ac.signal, deliverable: () => !res.writableEnded && !res.destroyed }, withElicit)
1000
509
  : withElicit();
1001
- // [816]/[820]②: the live tool-approval context — SAME id/owner/emit/signal as question/elicit
1002
- // (nested ALS, each its own store). sessionId keys the "allow all edits this session" state.
1003
- // [1535] 正名:本 sync 腿仍是唯一 runWithContext 包裹,但不再是子代唯一到达路——下方
1004
- // spec.onAsk 的 bound 闭包经 core 继承链给了 bg/嵌套子代直达宿主流的路;真正无宿主流的
1005
- // 残余腿(durable-submit/headless resume)才落 "unavailable" → durable park / core deny。
1006
- // 🔒 approval 帧不用共享 emitAsk 的静默 no-op 语义:res 已结束时 no-op = 卡「假投递成功」,
1007
- // coordinator 等满 TTL 落 deny —— G1 "unavailable"→park 腿在真 wiring 里就不可达(cross-review
1008
- // 抓获)。这里 THROW,coordinator 的 emit-catch 按「无活人可达」返 "unavailable"(durable 部署
1009
- // park;非 durable 由 core fail-closed deny,行为不劣于旧 no-op)。
1010
- // 当年本注写的「仅 approval 帧收紧」已被 #173 推进一格:question 帧此后与本臂**同形**收紧(见上面
1011
- // `emitQuestion`)。**HITL 三族里**仍留在共享 emitAsk 静默 no-op 上的只有 elicitation(其 decline
1012
- // 结算本身就是 fail-closed,收紧收益小,另议);emitAsk 本身还驮着 SendUserFile 的 `file_link` 等
1013
- // 非 HITL 帧,那些不在本条收紧的辖域内(file_link 的补偿是它自己的 durable 双写,不是 throw)。
1014
510
  const emitApproval = (frame) => {
1015
511
  if (res.writableEnded || res.destroyed)
1016
512
  throw new Error("live stream ended — approval card undeliverable");
1017
513
  emitAsk(frame);
1018
514
  };
1019
- // ── #151 车3 刀 3b:sync 腿的流内审批协议装配 ────────────────────────────────────────────
1020
- // 上场判据走**单一谓词**(与协调器注入 / `/v1/capabilities` / 回决端点 501 同一个符号)。
1021
- // 关着 ⇒ 下面三样(呈卡口 / 腿轴真值 / windowZero 分支)全部不接,本腿字节逐字不变(A-1)。
1022
515
  const streamApprovalOn = resolveStreamApprovalGate({
1023
516
  toolApprovalEnabled: Boolean(deps.toolApproval),
1024
517
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
@@ -1026,10 +519,6 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1026
519
  parkFacility: deps.checkpointStore !== undefined,
1027
520
  }).active;
1028
521
  const legWalltimeMs = typeof prepared.spec.limits?.maxWalltimeMs === "number" ? prepared.spec.limits.maxWalltimeMs : undefined;
1029
- // §7.3 + §8.3:本腿的「上不上场 / 窗是不是 0 / leg deadline 真值」走**三腿共用**的那一份判据
1030
- // (`resolveApprovalLeg` 顶注记着为什么必须是一个函数:第一版只写在本腿上,bg/resume 两腿因此
1031
- // 在开关关时行为变了、窗=0 也不生效)。取值点 = **本腿开始执行的时刻**(这里就是 `streamBody`
1032
- // 进入前的最后一站),误差方向偏短=安全侧。
1033
522
  const approvalLeg = resolveApprovalLeg({
1034
523
  streamApprovalOn,
1035
524
  windowMs: deps.config.streamApproval?.windowMs ?? 0,
@@ -1038,17 +527,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1038
527
  nowMonotonicMs: performance.now(),
1039
528
  });
1040
529
  const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
1041
- // 🔴 ①`forceDurableGate` principal 这一源**本装配点读不到**(`runtimeCapsResolver` 是 core 在
1042
- // prepare 内部按 principal 异步解析的 deps 级 seam)。按 §14 §12-5 亲裁:不可读则该源不做 ——
1043
- // core 自己兜得住(`prepare-task.js:3330` 的 `runtimeCaps?.forceDurableGate !== true` 让
1044
- // suspendAsk 不退位 ⇒ 结构性 park)。代价只是这类 principal 可能先收到一张必被 park 的卡,
1045
- // 已写进 wire 契约文档。
1046
530
  const windowZero = approvalLeg.windowZero;
1047
- // §4.3(a)+(c):呈卡帧的投递口 —— live SSE **与** detach 车道的 durable 账本**双写**。
1048
- // 🔴 codex round2 R2-1 修:两个 sink 的成败判定收进**唯一属主** `createApprovalCardEmitter`
1049
- // (顶注写着为什么「两路都没接下就必须抛」——静默成功会压住 park 路由,把一只没人能回答的 ask
1050
- // 挂到窗到期)。本处只负责把两个 sink 交出去;`writeLive` 在流已结束时**缺席**(不是 no-op),
1051
- // 于是「socket 已死 ∧ 账本写失败」如实成为一次投递失败。
1052
531
  const emitCard = createApprovalCardEmitter({
1053
532
  ...(ledgerSink
1054
533
  ? {
@@ -1064,20 +543,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1064
543
  emitAsk(frame);
1065
544
  },
1066
545
  });
1067
- // 车6 撤卡帧:**live only**(不进 durable tail —— 收敛器/reaper 那类调用点没有 seq 可分配,
1068
- // 硬塞进账本会铸一条序号伪造的行;丢帧的结构补偿恒是重连 preamble 的全量对账基准)。
1069
546
  const emitRevokeLive = (frame) => {
1070
547
  if (!res.writableEnded && !res.destroyed)
1071
548
  emitAsk(frame);
1072
549
  };
1073
- // [1535] server 半场([1539] 定谳断点①的修):同一 ctx 双装——ALS 包裹(宿主自身 ask 的
1074
- // 既有路)+ **spec.onAsk = boundAsk(ctx)**(core 1.294 继承链 parentConstraints 冻结
1075
- // `spec.onAsk ?? deps.onAsk` 逐 ancestor 传给每个委派子代:bg 子代 lane 无 ALS ctx 时不再
1076
- // 「unavailable/零帧 fail-closed deny」,权限卡带 sourceTaskId 浮到宿主本流)。suspendAsk
1077
- // 退位条件的「onAsk 在场」真值不变(deps.onAsk 恒 wire),durable 部署行为零漂移。
1078
- // #151 车3 刀 3b:`windowZero` ⇒ **不包 ALS**(ALS 腿一并让位,`withApproval` 退化成
1079
- // `withQuestion`),且下面的 `spec.onAsk` 换成 immediate-unavailable 闭包 —— 两件必须同时做,
1080
- // 只做一件的话另一条路仍会呈卡(ALS 腿是 `deps.onAsk` 的到达路)。
1081
550
  const approvalCtx = deps.toolApproval && !windowZero
1082
551
  ? {
1083
552
  taskId: askTaskId,
@@ -1085,13 +554,10 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1085
554
  emit: emitApproval,
1086
555
  abortSignal: ac.signal,
1087
556
  ...(prepared.spec.sessionId ? { sessionId: prepared.spec.sessionId } : {}),
1088
- // 协议上场才接新协议族的两个口与两条腿轴;关着时逐字保持既有四键形。
1089
557
  ...(streamApprovalOn
1090
558
  ? {
1091
559
  emitCard,
1092
560
  emitRevoke: emitRevokeLive,
1093
- // sync 腿恒是**首腿**:它不经 checkpoint token 复活(那条是 resume 腿的形),
1094
- // 空串是首腿的**真值**而不是「未知」(见 ToolApprovalRunContext.legKey 顶注)。
1095
561
  legKey: "",
1096
562
  ...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
1097
563
  }
@@ -1099,20 +565,7 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1099
565
  }
1100
566
  : undefined;
1101
567
  if (deps.toolApproval) {
1102
- // 🔴 具名一次再进闭包(自审:`deps.toolApproval!` 的非空断言在这里是**可避免**的 —— 断言只
1103
- // 在本 if 成立时正确,而闭包是稍后才跑的;拿捕获到的常量才是真的把那条不变量携进去)。
1104
568
  const toolApproval = deps.toolApproval;
1105
- // [1546] HIGH-1:闭包只携身份——emit 经 broker 取「当前」活跃流(宿主重连的新 SSE 在
1106
- // runWithContext 注册时覆盖指针),internalsSnapshot 冻结的闭包不再携死 response/死 signal。
1107
- // #151 车3 刀 3b(§8.3 ⑦):`windowZero` ⇒ 换成**立即** `"unavailable"` 的闭包。为什么不是
1108
- // 「不装 onAsk」:`deps.onAsk` 恒 wire(boot/runner-deps.ts)且 core 取 `spec.onAsk ?? deps.onAsk`,
1109
- // per-task 的「缺席」结构上不可达;而 core 的 `approverUnavailable === true` 分支
1110
- // (prepare-task.js:3328-3335)**直接绕过** onAsk 在场判定去铸 checkpoint —— 与「onAsk 缺席」
1111
- // 在 park 结局上逐字等价,且可 per-task 表达。
1112
- // #280 R-13 C 臂(d):这条短路的取值走协调器公开的**同一个**政策口
1113
- // (`unattendedAskOutcome()`)—— `UNATTENDED_APPROVAL_POLICY=deny` 的部署上它是裸 `false`。
1114
- // 🔴 不在这里读 `deps.config` 自己算一遍:五臂一处施加是本件的硬条款,两处各算必然漂
1115
- // (协调器与装配层对同一只 ask 给出不同终局)。
1116
569
  prepared.spec.onAsk = windowZero
1117
570
  ? () => Promise.resolve(toolApproval.unattendedAskOutcome())
1118
571
  : toolApproval.boundAsk({
@@ -1123,10 +576,6 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1123
576
  });
1124
577
  }
1125
578
  const withApproval = () => approvalCtx && deps.toolApproval ? deps.toolApproval.runWithContext(approvalCtx, withQuestion) : withQuestion();
1126
- // SendUserFile:sync 腿 file_link 走 live out-of-band 帧面(emitAsk)。detach 车道([1840]§五
1127
- // file_link 修):**同落账**——断连后 live 面写在已死 socket 上被丢,壳换道 events tail 必须
1128
- // 能拿到文件卡(bg 腿本就 durable append,双腿对齐);live+durable 双写与 text/tool 帧的换道
1129
- // 语义同栏(壳按换道协议处理重放)。非 detach 车道 ledgerSink 恒 undefined = live-only 字节不变。
1130
579
  await (deps.sendUserFile
1131
580
  ? deps.sendUserFile.runWithContext({
1132
581
  taskId: askTaskId,
@@ -1134,50 +583,33 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1134
583
  void (await emitAsk(f));
1135
584
  if (ledgerSink) {
1136
585
  const { type, ...rest } = f;
1137
- await ledgerSink.append(type, rest).catch(() => undefined); // 账本瞬断不 fault 工具面(live 帧已出)
586
+ await ledgerSink.append(type, rest).catch(() => undefined);
1138
587
  }
1139
588
  },
1140
589
  }, withApproval)
1141
590
  : withApproval());
1142
- // Drive the parked run row to its terminal/suspended state (releases or keeps task_active).
1143
591
  if (durableTaskId && deps.runStore) {
1144
- // R8 decouple defense-in-depth (review LOW): only KEEP the lock (setSuspended/needs_review) when
1145
- // checkpointStore exists — without it there's no resume ctx, so a suspend (unreachable per durableEnabled,
1146
- // but fail-safe matters) FALLS THROUGH to setTerminal which RELEASES the claim. Mirrors the sync path.
1147
592
  if (finalResult?.status === "suspended" && deps.checkpointStore)
1148
593
  await deps.runStore.setSuspended(durableTaskId);
1149
594
  else if (finalResult?.status === "needs_review" && deps.checkpointStore)
1150
- await deps.runStore.setNeedsReview(durableTaskId); // D-B: park a review pause, keep the lock (a plan_review GATE resumes via /plan_review; a needs_review gate = dry-run review has no resolution endpoint today — [3683]-1, don't copy this comment as "needs_review → /plan_review")
595
+ await deps.runStore.setNeedsReview(durableTaskId);
1151
596
  else if (finalResult)
1152
597
  await deps.runStore.setTerminal(durableTaskId, finalResult.status, finalResult, finalResult.errorMessage ?? null);
1153
598
  else {
1154
- // No done event observed — the client disconnected (closed → break) and ac.abort() killed the
1155
- // run. Race guard: the task may have SUSPENDED right before the abort (checkpoint already
1156
- // persisted, its done event unread) — a pending checkpoint outlives the abort, so park the row
1157
- // suspended (session stays locked for the operator's /decide). Otherwise record an honest
1158
- // failure: never "completed" for a run whose terminal state was never seen.
1159
- // R8: anchor the rewind handle even on disconnect — if the user-message entryId was committed
1160
- // before the Ctrl+C, the shell (holding the early run_started taskId) can still rewind to this turn.
1161
599
  putRewindAnchor();
1162
600
  const pending = await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId);
1163
601
  if (pending) {
1164
- // [1.211 codex M1] detach 账本补 park 终局帧(done 未读到但 checkpoint 已在——账本没有
1165
- // suspended 帧则 events tail 无终局,壳等空;gate 内容未读到 done 无从知,落 gate:null)。
1166
602
  if (ledgerSink && !ledgerTerminal) {
1167
603
  ledgerTerminal = true;
1168
604
  await ledgerSink.flush().catch(() => undefined);
1169
605
  await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
1170
606
  }
1171
607
  await deps.runStore.setSuspended(durableTaskId);
1172
- settleFleet("suspended"); // parked → stays in fleet as "waiting"
608
+ settleFleet("suspended");
1173
609
  }
1174
610
  else {
1175
- // codex L1 对齐:同一类用户中止(disconnect abort)不再因 iterator 正常 return / throw 而给
1176
- // 两种终局形——与 throw 路兜底、resume-abort settle 同为 errorCode "cancelled" 的取消终局。
1177
611
  const dcErr = "stream client disconnected before completion (run aborted)";
1178
612
  const dc = { taskId: durableTaskId, sessionId: prepared.spec.sessionId ?? "", status: "failed", errorCode: "cancelled", errorMessage: dcErr, stats: { turns: 0, tokens: 0 } };
1179
- // [900]① detach 车道:账本也要有终局帧(bg 腿 cancelled 终局同形;detach 下本分支仅在
1180
- // generator 无 done 早收/cancel-verb 干净收尾时可达)。best-effort:落账失败不改行既有 settle。
1181
613
  if (ledgerSink && !ledgerTerminal) {
1182
614
  ledgerTerminal = true;
1183
615
  await ledgerSink.flush().catch(() => undefined);
@@ -1187,67 +619,47 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1187
619
  settleFleet("failed");
1188
620
  }
1189
621
  }
1190
- runRowSettled = true; // [868]③: every branch above recorded the row — the finally backstop stands down
622
+ runRowSettled = true;
1191
623
  }
1192
624
  }
1193
625
  finally {
1194
- syncLegLive = false; // notifications from here on take the durable-inbox path
1195
- unregisterEngineNotice(); // #310:腿结束即注销 wire 口(留着 = 下一条同会话腿的通告投进一条死连接)
626
+ syncLegLive = false;
627
+ unregisterEngineNotice();
1196
628
  if (durableHeartbeat)
1197
629
  clearInterval(durableHeartbeat);
1198
- // 1.207: drop the live verb handle — IDENTITY-GUARDED (review LOW): a suspend→fast-resume can
1199
- // re-register the SAME taskId from the resume leg before this finally runs; an unconditional
1200
- // delete would clobber the fresh handle (verbs would 409 "another replica" until re-registration).
1201
630
  if (durableTaskId && steerableRuns.get(durableTaskId) === liveStreamRef)
1202
631
  steerableRuns.delete(durableTaskId);
1203
- // [888]① same identity guard for the cancel handle (a fast-resume registers its own controller).
1204
632
  if (durableTaskId && inflightRuns.get(durableTaskId) === ac) {
1205
633
  inflightRuns.delete(durableTaskId);
1206
- cancelledViaVerb.delete(durableTaskId); // H2 label lifecycle rides the registration
634
+ cancelledViaVerb.delete(durableTaskId);
1207
635
  }
1208
636
  for (const evict of subagentHandleEvictions)
1209
- evict(); // design/122 ③: leg-end handle eviction (identity-guarded)
1210
- // [868]③: the DURABLE twin of leak C — a throw out of the stream (abort mid-iteration / plain
1211
- // Error) also skipped the run-row settle block, leaving a `running` zombie that only reapStale
1212
- // would flip (after runStaleSec, as a misleading "run stalled (instance lost?)"). Settle it here:
1213
- // re-read the row (a concurrent path may have settled it — then this is a no-op), honour a
1214
- // suspend that raced the abort (pending checkpoint ⇒ park, the disconnect branch's exact guard),
1215
- // else fail it — errorCode "cancelled" when the leg was aborted (user kill / disconnect), bare
1216
- // failed for a genuine mid-stream error. Best-effort: the backstop must never mask the real throw.
1217
- // 🔴 runs BEFORE the leak-C fleet backstop below (codex M3): the run-row verdict here (park vs
1218
- // fail) must drive the fleet verdict — the old order settled the fleet row "failed" first and a
1219
- // subsequently PARKED run row contradicted it ("waiting" run under a failed fleet row).
637
+ evict();
1220
638
  if (!runRowSettled && durableTaskId && deps.runStore) {
1221
639
  try {
1222
640
  const row = await deps.runStore.getRun(durableTaskId);
1223
641
  if (row?.status === "running") {
1224
642
  const pending = prepared.spec.sessionId ? await deps.checkpointStore?.findPendingTokenBySession(prepared.spec.sessionId) : undefined;
1225
643
  if (pending) {
1226
- // [1.211 codex M1] detach 账本补 park 终局帧(finally 兜底路同款,gate 未知落 null)。
1227
644
  if (ledgerSink && !ledgerTerminal) {
1228
645
  ledgerTerminal = true;
1229
646
  await ledgerSink.flush().catch(() => undefined);
1230
647
  await ledgerSink.appendParked("suspended", {}).catch(() => undefined);
1231
648
  }
1232
649
  await deps.runStore.setSuspended(durableTaskId);
1233
- settleFleet("suspended"); // parked → the fleet row stays honest ("waiting", disconnect-branch parity)
650
+ settleFleet("suspended");
1234
651
  }
1235
652
  else if (finalResult) {
1236
- // [1.211 codex H1] done 已观测(账本/fleet 可能已按它落)但行仍 running = 首次 setTerminal
1237
- // 抖动——兜底必须重试「原终局」,绝不写 failed(否则账本 done+fleet completed+行 failed
1238
- // 三面矛盾;bg 腿 `reached` 重试同款纪律)。账本侧若 onDone 也没落成,这里补一发。
1239
653
  if (ledgerSink && !ledgerTerminal) {
1240
654
  ledgerTerminal = true;
1241
655
  await ledgerSink.onDone(finalResult).catch(() => undefined);
1242
656
  }
1243
657
  await deps.runStore.setTerminal(durableTaskId, finalResult.status, finalResult, finalResult.errorMessage ?? null);
1244
- settleFleet(finalResult.status, fleetRunResiduals(finalResult)); // once-guard 下通常 no-op;[1839]①/(a) 残局键同带
658
+ settleFleet(finalResult.status, fleetRunResiduals(finalResult));
1245
659
  }
1246
660
  else if (ac.signal.aborted) {
1247
661
  const err = "stream aborted before completion (run cancelled)";
1248
662
  const c = { taskId: durableTaskId, sessionId: prepared.spec.sessionId ?? "", status: "failed", errorCode: "cancelled", errorMessage: err, stats: { turns: 0, tokens: 0 } };
1249
- // [900]① detach 车道:throw 路兜底同样补账本终局帧(bg 腿 catch 路 append("failed") 同形;
1250
- // ledgerTerminal 防对已落 done/park 的账本覆写 failed —— bg 腿 `reached` 同款纪律)。
1251
663
  if (ledgerSink && !ledgerTerminal) {
1252
664
  ledgerTerminal = true;
1253
665
  await ledgerSink.flush().catch(() => undefined);
@@ -1267,28 +679,17 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1267
679
  }
1268
680
  }
1269
681
  catch {
1270
- /* best-effort — reapStale remains the last-resort net; the original throw stays the story */
1271
682
  }
1272
683
  }
1273
- // C: a plain Error thrown mid-stream skipped every onTerminal branch above — settle the leaked
1274
- // "running" row to "failed" before the throw propagates (no-op when any branch — incl. the [868]③
1275
- // park above — already settled; settleFleet is once-guarded).
1276
684
  settleFleet("failed");
1277
685
  }
1278
686
  return { status: 200, body: finalResult };
1279
687
  };
1280
- // B3 复审(codex HIGH):无 runStore 的部署 durableTaskId 恒缺 → 这条活流从不进 steerableRuns,
1281
- // inflight() 全程读 0,SIGHUP 空闲窗(以及 SIGTERM drain)会把它当空闲斩掉。整腿显式计数,
1282
- // 与 steerableRuns 的短暂双计无害(两者同在终局递减,>0 语义只看非零)。
1283
688
  counters.uncountedBillableInflight++;
1284
689
  try {
1285
- resp = await idemCache.run(idemKey, runStreamSubmitLeg,
1286
- // Cache only a clean completion; a failed/aborted stream is evicted so a retry re-runs.
1287
- (r) => r.body?.status === "completed");
690
+ resp = await idemCache.run(idemKey, runStreamSubmitLeg, (r) => r.body?.status === "completed");
1288
691
  }
1289
692
  catch (e) {
1290
- // Log unconditionally (don't silently drop a runner error that races with the client close — council);
1291
- // re-throw only while the client is still connected so the outer handler can surface it.
1292
693
  deps.logger?.error("stream_error", { closed, err: e instanceof Error ? e.message : String(e) });
1293
694
  if (!closed)
1294
695
  throw e;
@@ -1296,74 +697,43 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1296
697
  finally {
1297
698
  counters.uncountedBillableInflight--;
1298
699
  clearInterval(hb);
1299
- // E8 leak fence(双腿对齐的对偶):detach 腿注册过的归因键在 leg 收尾必 clear——终局 attach 已
1300
- // drain 过时这是幂等 no-op;errored/aborted leg 则丢掉未 drain 的累积(bg 腿 finally 同款)。
1301
700
  if (usageKey)
1302
701
  deps.modelUsage?.clear(usageKey);
1303
702
  }
1304
- // A concurrent caller the cache deduplicated (ranLive=false) received no live events — hand it the terminal.
1305
703
  if (!ranLive && resp && !res.writableEnded) {
1306
704
  sseData(res, { type: "done", result: resp.body, replay: true });
1307
705
  }
1308
706
  res.end();
1309
707
  }
1310
708
  else {
1311
- // Idempotency-Key dedup (center blocker: exactly-once billing) — concurrent + retried same-key submits
1312
- // share one run; the whole run+record is wrapped so metrics/cost are counted exactly once on a replay.
1313
- // ── B3 ── 同上:原文此处是同步腿的 **128 行匿名 async**,提为具名闭包。
1314
- // [3979]:带 verify/cascade 的 200 体附事件通道指路;plain 形缺席不铸键(负控钉在 audit-fixes-http)。
1315
- // 谓词包在**全部三处** 200 return 上而不只终局形:今天 verify/cascade 腿的 park 被 core 映射成
1316
- // failed(nested suspend)走终局,park 两形只可能 plain 抵达——但那是 core 的映射语义,不是本腿
1317
- // 可假设的不变式;包全三处让「core 让组合腿真 park」的未来变更不产生一个漏 notice 的形。
1318
709
  const withDiscoveryNotice = (body) => prepared.verify || prepared.cascade ? { ...body, notice: MULTI_ATTEMPT_NO_EVENT_CHANNEL_NOTICE } : body;
1319
710
  const runSyncSubmitLeg = async () => {
1320
- // Durable F4/ask on the sync path — same two things as the stream path (audit functional gap):
1321
- // 1. createRun a task_active claim at SUBMIT (+ heartbeat) so reapCtx's task_active guard protects
1322
- // the ctx for the WHOLE first leg (its updated_at is stamped at submit and never refreshed; a
1323
- // multi-minute leg would otherwise age it past REAP_RUN_STALE_SEC and get it reaped MID-RUN).
1324
- // setSuspended on suspend keeps the claim (session locked submit→resume); setTerminal releases it.
1325
- // 2. putCtx AFTER the claim → a suspend here is resumable on any replica. Claim-first is load-bearing
1326
- // (parity with /v1/runs): a lost claim is a 409, never a silent second leg on a locked session —
1327
- // and a rejected submit must not clobber the active run's resume ctx (putCtx is sessionId-keyed).
1328
711
  let durableTaskId;
1329
712
  let durableHeartbeat;
1330
- // R8 (CC parity): the run-row claim + `durableTaskId` (the REWIND anchor key + the unique result.taskId
1331
- // re-stamp) is gated on `runStore` ALONE; the durable-suspend resume ctx (`putCtx`) on `checkpointStore`. These
1332
- // were bundled under ONE checkpointStore gate, which over-coupled REWIND to DURABLE_APPROVAL — but E18 resume-at
1333
- // needs NO checkpoint (main.ts:815). Now matches the already-decoupled /v1/runs bg path: rewind works with just
1334
- // runStore (DB_BACKEND=mysql|pg|local), no approval. Claim-first preserved (a lost claim → 409, never a silent 2nd leg).
1335
713
  if (deps.runStore && prepared.spec.sessionId) {
1336
714
  const tid = uuidv7();
1337
715
  const created = await deps.runStore.createRun(tid, prepared.spec.sessionId, principal ?? null, deps.instanceId ?? "default", runMeta(prepared, source));
1338
716
  if (created.ok)
1339
- clearTurnActivity(tid); // #245 S1(codex S1-F2):清同 taskId 旧世代活性残留(runs.ts 同款注)
717
+ clearTurnActivity(tid);
1340
718
  if (!created.ok)
1341
- return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId) }; // [2255]① 真出路材料
1342
- deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id); // fire-and-forget;model 跟 turn([1992]②)
719
+ return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, prepared.spec.sessionId, created.activeTaskId) };
720
+ deps.sessionTitler?.maybeTitle(prepared.spec.sessionId, prepared.spec.objective, typeof prepared.spec.model === "string" ? prepared.spec.model : prepared.spec.model?.id);
1343
721
  durableTaskId = tid;
1344
722
  }
1345
723
  if (deps.checkpointStore && prepared.spec.sessionId) {
1346
- // putCtx AFTER the claim (resumable). If this throws, the claim row self-heals: no heartbeat yet → reapStale flips it failed + releases.
1347
724
  await deps.checkpointStore.putCtx(prepared.spec.sessionId, { body: prepared.body, memoryScope: prepared.auth?.memoryScope });
1348
725
  }
1349
- // [888]① interactive live-run cancel, non-stream twin: this leg previously ran with NO abort seam at
1350
- // all (bare runTask, no spec.signal) — a cancel flipped the durable flag nothing consumed. Wire a
1351
- // controller through spec.signal (same seam as the stream leg's disconnect-kill) + register it in the
1352
- // in-flight map so the cancel fast path lands. Children reaped by this abort are a USER stop (parity
1353
- // with the stream leg's disconnect marker).
1354
726
  const syncCancelCtrl = new AbortController();
1355
727
  if (prepared.spec.sessionId)
1356
- markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal); // owner=canonical taskId([3806] 后=durableTaskId;codex F4 连带,流式腿同修
728
+ markChildrenStoppedByUserOnAbort(syncCancelCtrl.signal, durableTaskId ?? prepared.spec.sessionId, prepared.auth?.principal);
1357
729
  if (durableTaskId && deps.runStore) {
1358
730
  const rs = deps.runStore;
1359
731
  const tid = durableTaskId;
1360
- const owner = principal ?? null; // run created with this owner — pass it as the single-DB null-safe heartbeat guard
732
+ const owner = principal ?? null;
1361
733
  const abortFromVerb = () => {
1362
- cancelledViaVerb.add(tid); // H2: label the settle (errorCode "cancelled")
734
+ cancelledViaVerb.add(tid);
1363
735
  syncCancelCtrl.abort();
1364
736
  };
1365
- // [1.207 codex H1] heartbeat ALSO polls the durable cancel flag (bg-leg parity) — cross-replica
1366
- // cancel only sets the flag; this leg previously had no consumer.
1367
737
  durableHeartbeat = setInterval(() => {
1368
738
  void rs.heartbeat(tid, owner).catch(() => undefined);
1369
739
  if (!syncCancelCtrl.signal.aborted) {
@@ -1373,30 +743,17 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1373
743
  }, HEARTBEAT_MS);
1374
744
  durableHeartbeat.unref?.();
1375
745
  inflightRuns.set(tid, syncCancelCtrl);
1376
- // [1.207 codex H1] registration-window catch-up (flag flipped during the awaited putCtx window).
1377
746
  void Promise.resolve(rs.isCancelRequested?.(tid, owner)).then((c) => { if (c && !syncCancelCtrl.signal.aborted)
1378
747
  abortFromVerb(); }).catch(() => undefined);
1379
748
  }
1380
749
  else if (durableTaskId) {
1381
750
  inflightRuns.set(durableTaskId, syncCancelCtrl);
1382
751
  }
1383
- const specWithSignal = { ...prepared.spec, signal: syncCancelCtrl.signal, ...(durableTaskId ? { taskId: durableTaskId } : {}) }; // [3806]:同流式腿——tid 进 spec.taskId,否则 core fallback sessionId 污染 lastRunId
1384
- // Developer mode (1.44): verify = adversarial verify→fix gate. Cascade (1.45): cheap→strong ladder,
1385
- // escalate on a failed gate (default = didn't complete). Both opt-in, mutually exclusive, multi-attempt.
752
+ const specWithSignal = { ...prepared.spec, signal: syncCancelCtrl.signal, ...(durableTaskId ? { taskId: durableTaskId } : {}) };
1386
753
  let result;
1387
- let cancelVerbLabel = false; // [1.207 codex H2] set in the finally below while the label entry is alive
1388
- // E8 (shell-host contract): the SYNCHRONOUS /v1/tasks path is INTENTIONALLY excluded from the per-task `modelUsage`
1389
- // echo — it does NOT register() with the ModelUsageTracker, so the register-gate makes its brain.call usage a
1390
- // no-op (NOT a leak; the register-gate prevents the prior unbounded growth). modelUsage is the
1391
- // async durable /v1/runs feature (cap `modelUsage` is gated on the run store + tracker); a sync caller gets
1392
- // its result synchronously and reads aggregate cost from stats. (Wiring sync = register + a withModelUsage
1393
- // attach pass; deferred as low-value for the synchronous path.)
1394
- counters.uncountedBillableInflight++; // B-1: the sync await is real model work — drain must see it
754
+ let cancelVerbLabel = false;
755
+ counters.uncountedBillableInflight++;
1395
756
  try {
1396
- // ── B3 ── 原文此处是一处三层三元,把三条模型驱动腿挤在五行里。三腿具名化后判别顺序
1397
- // (verify → cascade → plain)、短路语义、传参**逐字不变**;`runWithVerification` 仍是整对象
1398
- // 直传(快审 F1:cost 顶不在重建中丢失),`runCascade` 仍从操作员梯子 + 本任务预算现算配置。
1399
- // #196:三腿一律跑 `prepared.runner`(场景 hands 判别位选出的那只),不再直接摸 deps.runner。
1400
757
  const verifyLeg = (v) => runWithVerification(prepared.runner, specWithSignal, v);
1401
758
  const cascadeLeg = () => runCascade(prepared.runner, specWithSignal, cascadeConfig(deps.config.cascadeLadder, prepared.spec.limits?.maxCostUsd));
1402
759
  const plainLeg = () => prepared.runner.runTask(specWithSignal);
@@ -1407,67 +764,40 @@ async function handleTasksBody(req, res, url, ctx, miss) {
1407
764
  if (durableHeartbeat)
1408
765
  clearInterval(durableHeartbeat);
1409
766
  if (durableTaskId && inflightRuns.get(durableTaskId) === syncCancelCtrl) {
1410
- inflightRuns.delete(durableTaskId); // [888]① identity-guarded
1411
- cancelVerbLabel = cancelledViaVerb.has(durableTaskId); // H2: capture before dropping (projection below)
767
+ inflightRuns.delete(durableTaskId);
768
+ cancelVerbLabel = cancelledViaVerb.has(durableTaskId);
1412
769
  cancelledViaVerb.delete(durableTaskId);
1413
770
  }
1414
771
  }
1415
- // [1.207 codex H2] cancel-verb abort label (non-stream twin of the SSE done-frame projection):
1416
- // core's external-signal abort yields failed WITHOUT an errorCode — label it so /cancel's outcome is
1417
- // uniform across legs. Only an abort-shaped FAILED result; a completion that raced the abort stands.
1418
772
  if (cancelVerbLabel && result.status === "failed" && !result.errorCode) {
1419
773
  result = { ...result, errorCode: "cancelled" };
1420
774
  }
1421
- // E18: a resume-at caller error (a stale/non-message anchor target) comes back as a failed result carrying a
1422
- // resume_at.* code — a caller mistake that BILLED NOTHING. Handle it BEFORE the suspend/terminal/finalize
1423
- // logic below: release the submit-time claim (unlock the session) but do NOT finalizeTaskResult (it must not
1424
- // count as a billed failure / plan-cache miss), and return a 4xx (caller fault) instead of a 200-with-failed
1425
- // body. NOT idem-cached (the predicate below caches only success-range responses) — a fixed anchor can retry.
1426
775
  const resumeAtStatus = resumeAtHttpStatus(result);
1427
776
  if (resumeAtStatus) {
1428
777
  if (durableTaskId && deps.runStore)
1429
778
  await deps.runStore.setTerminal(durableTaskId, "failed", stripCheckpointToken(result), result.errorMessage ?? null);
1430
- // #148 件4: the transient org-memory refusal carries core's retry hint — forward it as
1431
- // retryAfterSec (seconds, ceil; family shape = usage.window_exhausted). ONLY on the transient
1432
- // code: a wait hint on a terminal 4xx would be a directional lie.
1433
779
  const retryAfterSec = result.errorCode === "memory.admission_required" && result.retryAfterMs !== undefined
1434
780
  ? Math.max(1, Math.ceil(result.retryAfterMs / 1000))
1435
781
  : undefined;
1436
782
  return { status: resumeAtStatus, body: { errorCode: result.errorCode, error: result.errorMessage ?? "resume-at failed", ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) } };
1437
783
  }
1438
- // Durable suspend on the sync path: PARK it as an async run + return a pollable taskId, NEVER the
1439
- // capability token. The submitter switches to GET /v1/runs/:id. Cost is recorded
1440
- // by the RESUMED run (mirrors runInBackground, which also parks-without-record on suspend) — not here,
1441
- // so the pre-suspend tokens aren't double-counted on resume.
1442
784
  if (result.status === "suspended" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
1443
- // The submit-time row — guaranteed when runStore is present (a lost claim was 409'd before running).
1444
785
  if (durableTaskId)
1445
786
  await deps.runStore.setSuspended(durableTaskId);
1446
787
  return { status: 200, body: withDiscoveryNotice({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "suspended" }) };
1447
788
  }
1448
- // design/80 D-B: a review pause (plan_review / dry_run_review) on the sync path — PARK it like a suspend
1449
- // (keep task_active) + return a pollable taskId. For a plan_review GATE the /v1/assistant/tasks/:id/plan_review
1450
- // resume claims it; a needs_review gate (dry-run review) has no resolution endpoint today ([3683]-1).
1451
- // NEVER the capability token (parity with the suspended branch above).
1452
789
  if (result.status === "needs_review" && deps.checkpointStore && deps.runStore && prepared.spec.sessionId) {
1453
790
  if (durableTaskId)
1454
791
  await deps.runStore.setNeedsReview(durableTaskId);
1455
792
  return { status: 200, body: withDiscoveryNotice({ taskId: durableTaskId, sessionId: prepared.spec.sessionId, status: "needs_review" }) };
1456
793
  }
1457
- // Not suspended → release the submit-time claim so the session unlocks + the ctx becomes reapable.
1458
794
  if (durableTaskId && deps.runStore)
1459
795
  await deps.runStore.setTerminal(durableTaskId, result.status, stripCheckpointToken(result), result.errorMessage ?? null);
1460
- // A completed OR failed result both BILLED → cache it so a retry replays the outcome (no double-bill);
1461
- // a thrown error / transient conflict is evicted by the cache so a genuine retry re-runs. (The async
1462
- // /v1/runs caches only its 202 receipt — a different object, queried separately — hence the asymmetry.)
1463
796
  finalizeTaskResult(result, principal, prepared.spec.objective, prepared.spec.sessionId);
1464
- // A verify/cascade NESTED suspend is mapped by core to status:"failed" while still carrying the
1465
- // capability token (the genuine top-level suspend is parked above and never reaches here). Strip it so
1466
- // the resume credential never goes in the response body OR the idempotency cache.
1467
797
  return { status: 200, body: withDiscoveryNotice(stripCheckpointToken(result)) };
1468
798
  };
1469
- const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400); // cache only success-range outcomes (a completed/failed BILLED result, replay to avoid double-bill); never a session-active 409 or a resume-at 4xx (billed nothing — a retry must re-check)
1470
- sendSubmitResult(res, resp); // 体里有 retryAfterSec ⇒ 标准 retry-after 头同值(见 sendSubmitResult 顶注)
799
+ const resp = await idemCache.run(idemKey, runSyncSubmitLeg, (r) => r.status < 400);
800
+ sendSubmitResult(res, resp);
1471
801
  }
1472
802
  return;
1473
803
  }