@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
@@ -14,7 +14,7 @@ import { normalizeApproachNotice, validateTaskAgents } from "../spec-fields.js";
14
14
  import { isValidCwd, MAX_ADDITIONAL_DIRS } from "../task-cwd.js";
15
15
  import { runInBackground, evictIfConflict, stripCheckpointToken, parkToolCallId, TurnAnchorCapture, HEARTBEAT_MS, markChildrenStoppedByUserOnAbort } from "../runs.js";
16
16
  import { readTurnActivityMs, recordTurnActivity } from "../turn-activity.js";
17
- import { publicStoreProbeError } from "../store-live-probe.js"; // #305:免鉴权 /health 面的探针错误闭集词表
17
+ import { publicStoreProbeError } from "../store-live-probe.js";
18
18
  import { looksLikeJwt } from "@sema-agent/registry-core/api/auth-bridge";
19
19
  import {} from "../orchestration/workflow-agent-steer.js";
20
20
  import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
@@ -27,15 +27,12 @@ import {} from "../observability/rate-limit.js";
27
27
  import { withPrincipal } from "../observability/principal-context.js";
28
28
  import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, wiringManifestEventData, humanInputEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "../trace/project.js";
29
29
  import { redactSecrets } from "../trace/redact.js";
30
- import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js"; // #253:resume-verify 臂的账本写口 = 与 bg/sync 两腿同一只投影件;#310:本腿账本写口的单写串行链
31
- import { registerEngineNoticeLeg } from "../trace/engine-notice-wire.js"; // #310:通告 wire 腿(resume 半场)
32
- import { recordFailOpen } from "../observability/fail-open.js"; // #310:通告 durable 写失败的留痕口
30
+ import { createLedgerSink, createSerialLedgerAppend } from "../trace/ledger-sink.js";
31
+ import { registerEngineNoticeLeg } from "../trace/engine-notice-wire.js";
32
+ import { recordFailOpen } from "../observability/fail-open.js";
33
33
  import { IdempotencyCache, scopedIdempotencyKey } from "./idempotency.js";
34
34
  export { scopedIdempotencyKey };
35
35
  import { streamSseLog } from "./sse-log.js";
36
- // design/97 R9: the shape's neutral leaf (route-ctx.ts type-imports VerifyRoundsSpec from here, not from this file).
37
- // `clampVerifyRounds` used to be re-exported from here for the tests; that re-export was deleted (8566348) and the
38
- // import sat on as a dead name — tests import it from `verify-rounds.js` directly, which is the single owner.
39
36
  import { verifyRoundsFromBody } from "./verify-rounds.js";
40
37
  import { RUN_NOT_FOUND_MESSAGE } from "./route-ctx.js";
41
38
  import { handleCapabilities } from "./routes/capabilities.js";
@@ -44,7 +41,7 @@ import { handleDiagnostics } from "./routes/diagnostics.js";
44
41
  import { handleAdoption } from "./routes/adoption.js";
45
42
  import { handleRetention } from "./routes/retention-ops.js";
46
43
  import { handleAdminDrain } from "./routes/admin-drain.js";
47
- import { handleAdminConfigRefresh } from "./routes/admin-config-refresh.js"; // #322 批1 追加件:配置手动刷新口(operator lane)
44
+ import { handleAdminConfigRefresh } from "./routes/admin-config-refresh.js";
48
45
  import { handleMemoryPolicy } from "./routes/memory-policy.js";
49
46
  import { handleMemoryBundle } from "./routes/memory-bundle.js";
50
47
  import { handleSharedMemory } from "./routes/shared-memory.js";
@@ -69,33 +66,20 @@ import { cascadeConfig } from "./run-meta.js";
69
66
  export { cascadeConfig };
70
67
  import { handleImages, createImagesLocal, coarseStatusForState, errorCodeForExit } from "./routes/images.js";
71
68
  export { coarseStatusForState, errorCodeForExit };
72
- // design/158 A9:装配器缝——发送器/身份门下沉到 http/ 叶子模块,routes/* 与 server.ts 共用同一实现
73
- // (routes/* 绝不可值 import server.ts:那条边会闭合运行时环,见 test/module-cycle-gate.test.ts)。
74
69
  import { sendJson, sendError, httpErrorCode, msg } from "./send.js";
75
70
  import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator } from "./principal-gate.js";
76
71
  import { buildActiveRunConflict, resumeEntryForGate } from "./active-run-conflict.js";
77
72
  export { explicitOperatorOk, isOperator };
78
- /** 分组入参 → 平铺视图(createHttpServer 的第一件事)。一组都没给 ⇒ 原样返回(存量调用零开销、零形变)。 */
79
73
  export function flattenServiceDeps(deps) {
80
74
  const { stores, coordinators, seams, observability, governance, deployment, knobs, ...flat } = deps;
81
75
  if (!(stores || coordinators || seams || observability || governance || deployment || knobs))
82
76
  return deps;
83
77
  return { ...stores, ...coordinators, ...seams, ...observability, ...governance, ...deployment, ...knobs, ...flat };
84
78
  }
85
- // design/158 A4: TaskRequestBody lives in ./wire-types.ts (a type-only leaf) — import it from there
86
- // ([2354] 兼容面全清:此处的兼容 re-export 已删;index.ts 公共面直接从 wire-types 转出)。
87
- /** Personalization caps: server-pinned hard limits — a UI may be stricter, never looser. */
88
79
  export const MAX_USER_SKILLS = 10;
89
- /** = core `SKILL_CONTENT_MAX_CHARS`(1MB 加载门,core 1.293 起 invoke 全文零截断、超 1MB 整拒;常量在
90
- * dist/core/runner/synthetic-tools.js:84,未从 core 包根导出,故钉本地孪生+本出处注记)。修7 曾对齐旧
91
- * 20k 截断门([845] 飞轮考据:20k 是压缩保留区口径,invoke 时刻用错),core 1.293 撤门后本门跟随回收
92
- * ([853]① 收口)——HTTP 面与引擎加载门同值,超限 400 fail-loud 而非引擎侧整拒后难诊断。 */
93
80
  export const MAX_SKILL_CONTENT_CHARS = 1_048_576;
94
81
  export const MAX_SYSTEM_PROMPT_CHARS = 16_384;
95
- /** Cap on a request's `outputSchema` (JSON-serialized) — an uncapped schema bloats the prompt + the per-turn
96
- * validation cost. 32KB comfortably holds a rich nested schema while bounding the abuse surface. */
97
82
  export const MAX_OUTPUT_SCHEMA_CHARS = 32_768;
98
- /** Validate `body.skills` (caps + shape). Returns an error string (→ 400) or null when acceptable. */
99
83
  export function validateUserSkills(skills) {
100
84
  if (skills === undefined)
101
85
  return null;
@@ -116,89 +100,26 @@ export function validateUserSkills(skills) {
116
100
  return null;
117
101
  }
118
102
  const MAX_BODY = 8 * 1024 * 1024;
119
- /** Caps on `body.images`: inline base64 `data` is otherwise passed RAW to the
120
- * spec (URL images are SSRF+size-gated by core, inline aren't). The whole JSON body is already ≤ MAX_BODY, but with NO
121
- * COUNT cap a request could carry thousands of tiny images (a decode/processing DoS), and there was no explicit
122
- * per-image error. The two-axis gate belongs in the server (shell carries content, server gates it). */
123
103
  const MAX_IMAGES_PER_REQUEST = 20;
124
- const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024; // a single inline image's base64 ≤ 6 MiB (< MAX_BODY, so REACHABLE — one image can't hog the whole 8 MiB budget); ~4.5 MiB decoded, generous for a screenshot/photo
125
- // Task Trace API (S1): GET /v1/tasks (list) | /v1/tasks/:id/turns | /v1/tasks/:id/stream. The `:id` group +
126
- // sub-route are both-or-neither, so a GET /v1/tasks/stream (no :id) does NOT match — the POST sync stream is unshadowed.
127
- // Hand (executionEnv) tool names core mounts at the runner when an executionEnvFactory is configured. These are
128
- // NEVER in spec.tools, so the durable-resume satisfiability pre-check must union them when remote exec is on.
129
- // #244([3751]): the authoritative set is the FULL band `HANDS_BAND_TOOL_NAMES` (capabilities/hands-lane.ts —
130
- // effects table + the mounted complement Monitor/EnterWorktree/ExitWorktree/SendMessage/AgentTranscript, retired
131
- // names filtered at the single owner). The old effects-only derivation left the complement out, so approving a
132
- // parked complement-tool ask (the disconnect force-park population, e.g. Monitor on the cli host lane) was a
133
- // permanent 422 conflict.pending_action_unsatisfiable — the re-presented card's answer could never propagate.
104
+ const MAX_IMAGE_BASE64_BYTES = 6 * 1024 * 1024;
134
105
  const HAND_TOOL_NAMES = HANDS_BAND_TOOL_NAMES;
135
- /** #233 / A-002.8 —— 未知模型 400 里 `available` 列表的**名单过滤**:不给用户广告他点不了的模型。
136
- * 名单缺席/空 ⇒ 与本件之前逐字同集(全目录去 `default` 别名);名单在场 ⇒ 只留名单准的那些。
137
- * 判定单源 `isModelAllowlisted`(与三个执法点同一只),所以"列出来的"与"点了不被拒的"恒为同一批。 */
138
106
  function mentionableNames(config, catalog) {
139
107
  const allowlist = config.atModelAllowlist ?? [];
140
108
  return Object.keys(catalog).filter((n) => n !== "default" && isModelAllowlisted(n, catalog, allowlist));
141
109
  }
142
- /** [1245] codex-3/4 — the REOPEN-class resume failures: core's runtask reopened the checkpoint (store.reopen,
143
- * dist-read) BEFORE returning this failed result, so the park is STILL PENDING and decidable. Everything
144
- * downstream must treat these as a re-park, never terminal: the durable event log writes `suspended` (not
145
- * `done`), the run row re-parks (keeps task_active), and /decide surfaces retriable:true. A reopen that
146
- * itself failed surfaces as `checkpoint.reopen_failed` — deliberately NOT matched here (terminal). */
147
110
  function isCheckpointReopenedFailure(r) {
148
111
  return r.status === "failed" && (r.errorCode === "resume.env_failed" || r.errorCode === "resume.tool_unavailable");
149
112
  }
150
- /**
151
- * 一条 verify/cascade 腿的结果**是不是嵌套 durable park**,以及 park 落在哪个词上(扫描P2 + codex 复审
152
- * round1 [high] 一,2026-08-12 亲读 core 验真)。`undefined` = 不是 park(照常走终局)。
153
- *
154
- * 🔴 判据是 **errorCode**,不是 `verification.unverifiedReason` —— 后者**不覆盖修复轮**:
155
- * · `verify.js:201/208`(runWithVerification / resumeWithVerification 的**立即** park)铸的 outcome 带
156
- * `unverifiedReason: status === "needs_review" ? "needs_review" : "suspended"`;
157
- * · 但 `verify.js:191`(verifier 判 FAIL 后的**修复轮** `runTask` 又停在门上)返回的是
158
- * `{ ...mapNestedSuspend(current), verification: outcome }`,那个 `outcome` 是**上一轮验证器的
159
- * FAIL 判决** —— `verdict:"FAIL"` 的臂根本不填 `unverifiedReason`(`verify.js:166` 只在
160
- * no_verdict / PARTIAL 两种情况填)。于是「按 reason 判 park」对修复轮恒为假,而结果里
161
- * checkpointToken 还活着 ⇒ 写 `done` + setTerminal ⇒ 释放 task_active 锁 + 孤儿化那张卡。
162
- *
163
- * `mapNestedSuspend`(core `agents/suspend-guard.js`)是**每一条**嵌套 park 的必经点,它无条件盖这两个
164
- * 码之一,并且码本身就带 park 的词 —— 所以它同时是「是不是 park」和「是哪个 park」的单一真源。
165
- * 两个字面量在 core 的 index 里没有导出,只能照字面锚(与 `runs.ts` / `resource-suspend.ts` 顶注同姿势)。
166
- *
167
- * ⚠️ 边界:`checkpoint.reopen_failed` 与 `resume.*` 两类 reopen 失败**不在**这里(它们不是 gate 再停,
168
- * 由 {@link isCheckpointReopenedFailure} + 店内复核那条腿处置);`verifier_paused` 也不在(那是验证器
169
- * 自己的会话与凭据,本 run 的活已经做完)。
170
- */
171
113
  function nestedParkStatusOf(r) {
172
114
  switch (r.errorCode) {
173
- case "unexpected.needs_review": // core UNEXPECTED_NEEDS_REVIEW —— 内层 status 是 needs_review 的 park
115
+ case "unexpected.needs_review":
174
116
  return "needs_review";
175
- case "unexpected.suspended": // core UNEXPECTED_SUSPENDED —— 内层 status 是 suspended 的 park
117
+ case "unexpected.suspended":
176
118
  return "suspended";
177
119
  default:
178
120
  return undefined;
179
121
  }
180
122
  }
181
- /** The TRUSTED end-user principal for an owner-gated / per-tenant request. On a direct-door worker the
182
- * trusted-header path is OFF (x-agent-principal is client-spoofable), so identity comes from the crypto-VERIFIED
183
- * principal JWT (`x-approval-principal-token`, principal-only proof — no cnf.bnd action binding); otherwise the
184
- * BFF-minted trusted header. undefined when absent/invalid (fail-closed → callers map to 401 / scope "__none__").
185
- * 🔴 Use this for ANY endpoint that reads or gates PER-TENANT data — NOT `principalFrom` directly, which trusts
186
- * the spoofable header on a direct door (/v1/usage + the trace owner-gate
187
- * had drifted to a bare principalFrom, unlike the leader/images/approvals identity paths). One source of truth
188
- * so a future per-tenant endpoint can't re-introduce the bypass. The action-BOUND decide/answer paths keep their
189
- * own verifyDirectDoorProof (cnf.bnd) — this is the identity-only twin. */
190
- // F-fix: a thin delegate to the single source of the verified identity (security.ts `verifiedPrincipal`). Kept as a
191
- // named alias so the existing call sites read clearly; the JWT logic now lives in one place that createAuthorizer
192
- // shares, so spec.principal (governance + cost) and these observability/owner sites can never drift apart again.
193
- /** design/158 A9 装配表(顺序 = 拆分前 `handle()` 里各域出现的先后,逐行同序;真正的 `return` 在 handle 内,
194
- * 这里只做**签名一致性**的编译期钉:任何域入口漂了形,是编译红,不是运行时静默 404)。
195
- *
196
- * 🔴 A-010.24(验真后修):上面那句「逐行同序」此前是**一句自述,没有任何东西执法**,而它已经不成立 ——
197
- * `handleDiagnostics` 从来没进过这张表,车二着陆后 `handleRules` / `handleSharedMemory` 的先后也反了。
198
- * 类型钉不住这两类漂:少一个成员、调换两个成员,`RouteHandler[]` 全都收下。而这张表是评审时唯一会被
199
- * 当成「域全集 + 域次序」来读的清单(新域该插哪儿、某域在谁之后才可达),它给错答案是要害。
200
- * 执法面现在真的有了:`test/route-shape-roster.test.ts` 的**层D**从 `handle()` 的真源码抽出域调用序,
201
- * 与本表**逐项**对拍 —— 漏员或乱序都指名道姓地红。改 handle() 的次序时,这张表要跟着改。 */
202
123
  const ROUTE_DOMAINS = [
203
124
  handleTraceUsage,
204
125
  handleWorkflows,
@@ -232,81 +153,17 @@ const ROUTE_DOMAINS = [
232
153
  handleNotifyWake,
233
154
  ];
234
155
  void ROUTE_DOMAINS;
235
- /**
236
- * Managed-Agents-style HTTP/SSE service surface.
237
- * GET /health
238
- * POST /v1/tasks → run to completion, TaskResult JSON (synchronous)
239
- * POST /v1/tasks/stream → SSE of TaskEvent, live tokens (synchronous, same instance)
240
- * POST /v1/runs → async: 202 {taskId, sessionId, status}, runs in background (S1)
241
- * GET /v1/runs/:id → run status + result (poll)
242
- * GET /v1/runs/:id/events → SSE replay from Last-Event-ID, then tail to terminal (S2)
243
- * POST /v1/sessions/:id/notify → inject an out-of-session event (design/144 §2): live stream ⇒ core
244
- * `TaskStream.notify()`; idle ⇒ parked in the session inbox, drained
245
- * as a `task_notification` on the next stream open
246
- * POST /v1/sessions/:id/wake → wake a `task_done` pure park with a message and resume it
247
- * (design/144 §3). NOT a gate decision — a pending gate is refused
248
- * (`wake.gate_pending`). Goes through `resumeWake`, which reuses the
249
- * resume family's lease/CAS/run-log leg, so it BURNS MODEL TOKENS and
250
- * is subject to the billable-submit gates (`SESSION_WAKE_RE`).
251
- * (both live in `http/routes/notify-wake.ts` — design/158 A9 domain split)
252
- */
253
156
  export function createHttpServer(rawDeps) {
254
- // design/158 A8:分组装配 → 平铺视图。**分组是装配面的形,不是消费面的形**——下面 ~700 处 `deps.x`
255
- // 消费点原样保留(平铺入参零形变、零开销:没给任何组时 flatten 原样返回同一个对象)。
256
157
  const deps = flattenServiceDeps(rawDeps);
257
- // Idempotency-Key dedup for task submission (center review-routing blocker: exactly-once billing). One
258
- // cache per server instance, shared by the sync /v1/tasks and async /v1/runs create paths.
259
158
  const idemCache = new IdempotencyCache();
260
- // In-flight durable runs on THIS instance → their cancel controller. POST /v1/runs/:id/cancel landing here
261
- // aborts immediately (fast path); a cancel landing on another replica uses the durable flag + heartbeat poll.
262
159
  const inflightRuns = new Map();
263
- // [1.207 codex H2] taskIds whose abort came from the CANCEL VERB (fast path or flag poll) — the interactive
264
- // legs consult this at settle time to label an abort-shaped failed result `errorCode:"cancelled"` (core's
265
- // external-signal abort yields failed WITHOUT an errorCode; first-writer-wins would then pin the unlabeled
266
- // form). A genuine completion that raced the abort is NOT relabeled (only status==="failed" gets the code).
267
- // Entries are dropped in each leg's finally (same lifecycle as its inflightRuns registration).
268
160
  const cancelledViaVerb = new Set();
269
- // design/80 seam #2 (preemption): in-flight PREEMPTIBLE runs on THIS instance → their preempt controller (the
270
- // graceful-yield twin of inflightRuns' kill). POST /v1/assistant/tasks/:id/preempt landing here aborts it
271
- // immediately (fast path → core durably suspends at the next clean turn boundary); a preempt landing on another
272
- // replica uses the durable `preempt_requested` flag + heartbeat poll. Only plain-stream + resumed legs register.
273
161
  const preemptableRuns = new Map();
274
- // design/47 + design/80 D-A (steering): in-flight STEERABLE runs on THIS instance → their live core TaskStream
275
- // handle. POST /v1/runs/:id/steer landing here injects a mid-task message via the stream's `steer()` (applied at
276
- // the next turn-boundary drain → 200). A run that is NOT here is either durably suspended (→ park the steer on its
277
- // checkpoint via setPendingSteer → 202) or streaming on another replica (cross-replica live-steer is a fast-follow,
278
- // single-replica today → 409). Only the plain-stream + resumed legs hold a live stream, so only they register
279
- // (the verify/cascade legs return a result, not a stream — nothing to steer, parity with preemptableRuns).
280
162
  const steerableRuns = new Map();
281
- // design/144 wake 半场:终态 steer 铸 park 的 per-session 串行链(复审 HIGH:tryPark→put 非原子,
282
- // 并发双 steer 双铸两个 pending park)。单副本部署(本仓 steer 跨副本 seam 同款现实)进程锁即闭环;
283
- // 跨副本残窗由 put 后赢家复核收敛兜底(见 (d) 分支),与 cross-replica live-steer 同列 fast-follow。
284
163
  const wakeParkMints = new Map();
285
- // design/158 A9:两条「裸 number 登记簿」装盒——`let` 拆不出闭包,域模块要自增/自减必须共享同一个对象。
286
- // 语义与计数时机逐字不变(uncountedBillableInflight = B-1: sync /v1/tasks + leader bg runs,无 map 身份,
287
- // drain 靠它看见;admittedInflight = 修1 的 admission 期计数,见下方注释)。
288
164
  const counters = { uncountedBillableInflight: 0, admittedInflight: 0 };
289
- // 修1(三路复审 B3 HIGH,迟登记窗):ADMISSION-time in-flight counter. Every non-probe request is counted
290
- // SYNCHRONOUSLY in handle()'s first statements (before ANY await — auth/prepareSpec/lease/claim all happen
291
- // later) and released once, on whichever of finish/close fires first. Closes the window where a request that
292
- // just entered the handler was invisible to inflight() and a SIGHUP idle check (or SIGTERM drain read)
293
- // misjudged the replica idle → hardShutdown mid-admission. Deliberately OVERLAPS the leg registries below
294
- // (a registered leg counts twice) — every consumer only tests inflight()===0, so overcount is conservative,
295
- // never a false-idle.
296
- // drain: expose this replica's in-flight leg count as a LIVE getter on the shared drain state. Union of
297
- // taskIds (a resumed leg registers in BOTH maps) — bg/resume durable legs (inflightRuns) + live streams
298
- // (steerableRuns, sync leg included since 1.78). main.ts polls this after SIGTERM to know when drain is done.
299
- // 对抗复查 B-1(CRITICAL):the SYNC /v1/tasks await and the leader background run register in NEITHER map —
300
- // a SIGTERM during either read inflight=0 and hard-shut mid-model-work. Count them explicitly (a plain
301
- // counter: they have no steer/resume identity to key a map on).
302
165
  if (deps.drainState)
303
166
  deps.drainState.inflight = () => new Set([...inflightRuns.keys(), ...steerableRuns.keys()]).size + counters.uncountedBillableInflight + counters.admittedInflight + (deps.leaderEndpoint?.inflight?.() ?? 0);
304
- // B3 复审(codex HIGH):admission 计数之外再补一条粗粒度活性时钟:任何非探针请求(/health、/metrics*
305
- // 除外——探针/抓取是常态噪音)都推进时间戳;SIGHUP 空闲窗要求「inflight 归零 + 活性静默」双满足才关停。
306
- // 顺带把 idle-but-live 的 peer(还在 list/浏览的壳)也保住。
307
- // ⚠️ 时基契约(三路复审修3):performance.now() 单调域,与 sighup-idle.ts 的 `now` seam 同域——挂钟
308
- // (Date.now)跳变(NTP 回拨/夏令时)会把空闲窗判定拉爆。对外报告的 epoch 时间戳(请求日志/drainState.since)
309
- // 不受影响,仍走 Date.now。
310
167
  let lastBillableActivityAt = performance.now();
311
168
  const noteActivity = (url) => {
312
169
  if (url === "/health" || url.startsWith("/metrics"))
@@ -315,22 +172,13 @@ export function createHttpServer(rawDeps) {
315
172
  };
316
173
  if (deps.drainState)
317
174
  deps.drainState.lastActivityAt = () => lastBillableActivityAt;
318
- // asyncRewake wake 管道(hooks 阶段二挂账兑现):hook-runner 的 ctx.wake 经 main.ts 的 hookWakeBus holder
319
- // 到这里——session → 本副本活流(steerableRuns 是 taskId 键,反查经 run 行;活流数=本副本并发数,量小;
320
- // wake 是稀有事件[async hook exit 2],逐行 get 可接受)。找到即 steer(操作者 hook 的 stderr,trusted
321
- // 与 explicitOperator 同源语义;下个 turn 边界注入)。无活流 → 挂起腿 park fallback(与 HTTP steer 的
322
- // tryPark 同一原语:pending checkpoint 是权威,CAS on status='pending';落上=resume 时 core 注入)。
323
- // park 也不上 → false(别副本 live/已结束——调用方记账)。
324
175
  if (deps.hookWakeBus) {
325
176
  deps.hookWakeBus.deliver = async (sessionId, text) => {
326
- // hook stderr 不是操作者亲笔——是 hook 脚本运行时输出(可能回显下载物/工具输出),
327
- // 【内容】不升 trusted(与 HTTP steer 的非-explicitOperator 姿势一致),且先过同一 validatePendingSteer
328
- // 校验(break-out/控制字符即拒,不静默换流)。围栏 provenance 在 main.ts 闭包侧(注入前缀标明来源)。
329
177
  try {
330
178
  validatePendingSteer({ text, trusted: false });
331
179
  }
332
180
  catch {
333
- return false; // steering.invalid_content:内容不合法,不投递(调用方按 dropped 记账)
181
+ return false;
334
182
  }
335
183
  for (const [tid, stream] of steerableRuns) {
336
184
  try {
@@ -341,12 +189,9 @@ export function createHttpServer(rawDeps) {
341
189
  return true;
342
190
  }
343
191
  catch {
344
- continue; // not_running race / 行读失败 → 试下一个;全空 → 挂起腿 park
192
+ continue;
345
193
  }
346
194
  }
347
- // 挂起腿:任务 durably suspended 时 async hook 才收场(如慢 lint 收尾)——
348
- // wake park 到 session 的 pending checkpoint(镜像 POST /v1/runs/:id/steer 的 tryPark:scope/token
349
- // 缺失或 CAS 失手=没有真挂起的 checkpoint → false)。resume 时 core 把它作为 pending steer 注入。
350
195
  try {
351
196
  const cs = deps.checkpointStore;
352
197
  if (!cs)
@@ -360,17 +205,12 @@ export function createHttpServer(rawDeps) {
360
205
  return await cs.setPendingSteer(token, scope, { text, trusted: false });
361
206
  }
362
207
  catch {
363
- // park 面任何失败=诚实 false(调用方按 dropped 记账),绝不 throw 回 hook 收尾路径。
364
- // core 5.14.0(#147)新增的 `steering.queue_full` 也落这里:队列满 ⇒ 这条 hook 输出确实没投出去,
365
- // `false` 就是它的真实结局。**不给它 wire 码**——本通道是进程内 hook 收尾,没有 HTTP 应答面。
366
208
  return false;
367
209
  }
368
210
  };
369
211
  }
370
212
  const server = http.createServer((req, res) => {
371
213
  void handle(req, res).catch((err) => {
372
- // A typed HttpError (e.g. a malformed JSON body → 400) carries its own status — a client mistake is a
373
- // 4xx, not a 500. Everything else is a genuine internal error: log the cause, never echo it (info disclosure).
374
214
  if (err instanceof HttpError) {
375
215
  if (!res.headersSent)
376
216
  sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.extra ?? {}) });
@@ -385,38 +225,22 @@ export function createHttpServer(rawDeps) {
385
225
  res.end();
386
226
  });
387
227
  });
388
- // design/158 A9 装配缝:服务级 ctx 建**一次**(deps 平铺视图 / 跨域运行期登记簿 / 无状态助手 / 单域状态)。
389
- // 每请求只再加一格 `req`(见 handle 内)。助手全是 `function` 声明,提升后此处引用安全。
390
228
  const routeCtxBase = {
391
229
  deps,
392
230
  registry: { idemCache, inflightRuns, preemptableRuns, cancelledViaVerb, steerableRuns, wakeParkMints, counters },
393
231
  helpers: { readJson, readRawBody, rateLimited, quotaExceeded, leaseDenied, usageWindowDenied, safeDecode, isFleetWide, runOwnerOk, sessionOwnerScope, sessionOwnerScopeForWrite, runSessionAcceptOk },
394
232
  local: { images: createImagesLocal(deps), sessions: createSessionsLocal(), sessionSync: createSessionSyncLocal() },
395
- // A9 尾批:提交/续跑「腿」只装引用(实现仍在本闭包里,逐字未动)——tasks/runs 共用 prepareSpec,
396
- // approvals/assistant/notify-wake 共用 resume 家族;走这一格才不会造出 routes/* 互相值 import 的环。
397
233
  legs: { prepareSpec, finalizeTaskResult, resumeCheckpoint, driveResumeIntoRunLog, resumePreempted, resumeWake, resumePlanReview },
398
234
  };
399
235
  async function handle(req, res) {
400
236
  const startedAt = Date.now();
401
237
  const method = req.method ?? "GET";
402
238
  const url = (req.url ?? "/").split("?")[0];
403
- noteActivity(url); // B3: admission-time activity stamp (before any await — no gap for the idle watch to miss)
404
- // 修1(三路复审 B3 HIGH):admission-time inflight registration — SYNCHRONOUS, before the first await, so a
405
- // request still in its pre-registration phase (auth/prepareSpec/lease/claim) is already visible to
406
- // drainState.inflight(). Released exactly once in `record` below (finish/close latch — the same lifecycle
407
- // that covers aborted SSE streams). Probes (/health, /metrics*) and CORS preflight stay uncounted (constant
408
- // sampling noise must not hold the SIGHUP idle window open).
239
+ noteActivity(url);
409
240
  const admitted = method !== "OPTIONS" && url !== "/health" && !url.startsWith("/metrics");
410
241
  if (admitted)
411
242
  counters.admittedInflight++;
412
- // Record on whichever of finish/close fires FIRST (a `logged` latch prevents double-count): a client that
413
- // aborts (esp. an SSE stream) fires `close` WITHOUT `finish`, so a finish-only handler never counts it —
414
- // exactly the abnormal traffic most worth observing. An abort mid-response is logged as 499 (client closed).
415
243
  let logged = false;
416
- // [854]⑥ 纯观测:tasks/stream 断连(499)日志此前无 task 上下文,排查「谁的流断了」只能对时间戳。
417
- // 流路由把 X-Task-Id(earlyDurableTid)stash 到这两个 handle 作用域变量;record 只在 499 行带上——
418
- // 不动计费/inflight(admittedInflight 的 latch 语义原样)。detached 标记见 [854]①b(断连宽限 opt-in)。
419
- // design/158 A9:每请求局部量收进 reqState(域模块经 ctx.req 读写同一个盒子),ctx = 服务级四格 + 本格。
420
244
  const reqState = { streamTaskId: undefined, streamDetached: false, source: null };
421
245
  const ctx = { deps: routeCtxBase.deps, registry: routeCtxBase.registry, helpers: routeCtxBase.helpers, local: routeCtxBase.local, legs: routeCtxBase.legs, req: reqState };
422
246
  const record = (viaClose) => {
@@ -424,7 +248,7 @@ export function createHttpServer(rawDeps) {
424
248
  return;
425
249
  logged = true;
426
250
  if (admitted)
427
- counters.admittedInflight--; // 修1: paired release (latched — one decrement per admitted request)
251
+ counters.admittedInflight--;
428
252
  const route = routeLabel(method, url);
429
253
  const seconds = (Date.now() - startedAt) / 1000;
430
254
  const aborted = viaClose && !res.writableEnded;
@@ -443,69 +267,32 @@ export function createHttpServer(rawDeps) {
443
267
  return;
444
268
  }
445
269
  if (req.method === "GET" && url === "/health") {
446
- // Sema-registry restart signal: additive, present ONLY when a restart-to-apply slice changed
447
- // since boot — so a normal worker's /health shape is unchanged. `restartRequired` is the boolean the
448
- // orchestrator polls; `restart` carries which slices + the version + when it first became required.
449
270
  const restart = deps.restartState?.();
450
- // TOC integration (#1 /health contract — INTEGRATION-DESIGN): identity fields so the shell's Lifecycle
451
- // Manager can verify it connected to the RIGHT engine (instance + data dir + config fingerprint + pid/port), not
452
- // a stale/wrong one. configHash = a STABLE fingerprint of the restart-relevant config shape (NO secrets — only
453
- // model id / backends / data root / provider / port), so the shell detects a config drift without reading values.
454
271
  const configHash = createHash("sha256")
455
272
  .update(JSON.stringify({ model: deps.config.model.id, sessionBackend: deps.config.sessionBackend, dbBackend: deps.config.dbBackend, dataRoot: deps.config.localDataRoot, configProvider: deps.config.configProvider, remoteExec: deps.config.remoteExec?.provider, port: deps.config.port }))
456
273
  .digest("hex")
457
274
  .slice(0, 16);
458
275
  sendJson(res, 200, {
459
276
  status: "ok",
460
- // build self-description — "did the restart actually pick up new code" judgement for the
461
- // shell/orchestrator (same source as GET /v1/capabilities.version; absent only if capabilities wiring is).
462
277
  ...(typeof deps.capabilities?.version === "string" ? { version: deps.capabilities.version } : {}),
463
- // additive draining flag — k8s readiness / shell Lifecycle Manager摘流 signal. Absent when not draining.
464
- // #291:drainReason = POST /v1/admin/drain 声明的停机因由(additive,未声明=键缺席)。
465
278
  ...(deps.drainState?.draining ? { draining: true, ...(deps.drainState.since ? { drainingSince: deps.drainState.since } : {}), ...(deps.drainState.reason ? { drainReason: deps.drainState.reason } : {}) } : {}),
466
- // #320②([4728] 五点令):in-flight 腿数**恒报**(不只 draining 时)——排空观测「还剩几条腿」
467
- // 与日常容量观察同一个键;additive,getter 未接线(drainState 缺席部署)= 键缺席形状不变。
468
- // 值取 createServer 自赋的同一只 getter(durable bg/resume inflightRuns ∪ live 流 steerableRuns
469
- // ∪ admission 高水位,见 drainState.inflight 契约注)——不另立第二判据。
470
279
  ...(deps.drainState?.inflight ? { inflight: deps.drainState.inflight() } : {}),
471
- // S5: additive degradation flag — present ONLY when the boot auto-probe fell back to in-memory, so a
472
- // healthy worker's /health shape is unchanged. durable:false = "this replica is NOT persisting although
473
- // a DB was configured" (the orchestrator's restart/alarm signal; an in-memory-by-choice worker omits it).
474
280
  ...(deps.storeDegraded ? { durable: false, storeDegraded: true } : {}),
475
- // boot ready 门(b):additive — absent = ready(正常形状不变);false = roster 未落地,
476
- // 计费提交在 503(orchestrator/壳读此旗决定摘流/重试;编排器侧「pull 成功才 ready」增强不互斥)。
477
281
  ...(deps.modelReady && !deps.modelReady() ? { ready: false, readyReason: "model roster pending (first effective-config pull)" } : {}),
478
282
  model: deps.config.model.id,
479
- sessionBackend: deps.sessionStoreLabel ?? deps.config.sessionBackend, // effective store, not the coerced enum
283
+ sessionBackend: deps.sessionStoreLabel ?? deps.config.sessionBackend,
480
284
  sessions: deps.runner.sessions.size,
481
285
  ...(deps.instanceId ? { instanceId: deps.instanceId } : {}),
482
- // 🔴 `dataRoot`/`pid` 是**公开承诺字段**(2026-07-28 起,USAGE「嵌入形契约」段):桌面宿主的
483
- // 引擎身份验证(「这个 /health 是不是我起的那个进程」)整个押在这两个键上——pid 必须等于所
484
- // spawn 子进程、dataRoot 必须等于所配数据根,缺任一=判外来。localDataRoot 的解析恒回退
485
- // `~/.ai-agent`(config.ts,与 DB_BACKEND 无关)⇒ 此条件 spread 实为恒真;改动这两个键的
486
- // 在场性/语义前先看 test/health-identity-contract.test.ts。
487
286
  ...(deps.config.localDataRoot ? { dataRoot: deps.config.localDataRoot } : {}),
488
287
  configHash,
489
288
  pid: process.pid,
490
289
  port: deps.config.port,
491
- // D-G: a POSITIVE go-live signal that this worker is a crypto-bound direct-door worker (trusted-header
492
- // path disabled) — additive, only present when active, so a normal worker's /health shape is unchanged.
493
- // Surfaced for the DG-M2 go-live runbook (was previously inferrable only behaviorally via a 401).
494
290
  ...(deps.config.directDoorActive ? { directDoorActive: true } : {}),
495
291
  ...(restart ? { restartRequired: true, restart: { reasons: restart.reasons, version: restart.version, since: restart.since } } : {}),
496
- // codex R18 (models-tiers deferral): additive DEGRADED state — the model plane is deferred but no
497
- // next-boot handoff exists (LKG not restart-surviving), so no restart is forced and the plane stays
498
- // unapplied. Present ⇒ the operator must either mount restart-surviving LKG storage (+ set
499
- // CONFIG_LKG_DURABLE) or restart manually after ensuring the candidate will be re-pulled at boot.
500
292
  ...(() => {
501
293
  const stuck = deps.planeDeferredState?.();
502
294
  return stuck ? { modelPlaneDeferred: { version: stuck.version, since: stuck.since, noHandoff: true, ...(stuck.blocked ? { blockedReasons: stuck.blocked } : {}) } } : {};
503
295
  })(),
504
- // #322 世代账三键(additive;未接线 ⇒ 键全缺席,/health 形状逐字不变)。
505
- // 语义:target = 本副本**最后**见到的源版本(身份标);applied = 最后一次真的 commit 的版本;
506
- // staleMs = 两者**持续**不等的时长(收敛即缺席)。#325 起 additive 补两 ordinal 键(收货序数,
507
- // 进程内单调)——local 腿 version 是内容哈希无序承诺,「见过多少代/停在哪一代」的推进判断
508
- // 用 ordinal 键;跨副本「同不同代」的对账仍用 version 身份标。
509
296
  ...(() => {
510
297
  const gen = deps.configApplyState?.();
511
298
  if (!gen)
@@ -518,25 +305,18 @@ export function createHttpServer(rawDeps) {
518
305
  ...(gen.configApplyStaleMs !== undefined ? { configApplyStaleMs: gen.configApplyStaleMs } : {}),
519
306
  };
520
307
  })(),
521
- // B1 活体 store 探测(座注见 storeLiveState 声明;钉 test/health-store-live.test.ts)
522
308
  ...(() => {
523
309
  const probe = deps.storeLiveState?.();
524
310
  if (!probe)
525
311
  return {};
526
312
  return {
527
313
  ...(probe.live ? {} : { storeLive: false }),
528
- // #305:error 过闭集词表 —— 这一整个响应体在鉴权门**之前**发出,而座席给的是驱动原始
529
- // message(DSN/主机/账号常在其中)。消毒钉在**这个 wire 装配点**而不是探针环里:座席是
530
- // 可注入接缝,只有边界上的这一道对**任何**生产者成立(fail-closed 方向)。原文留日志轴。
531
314
  storeProbe: { live: probe.live, ageMs: probe.ageMs, ...(probe.error !== undefined ? { error: publicStoreProbeError(probe.error) } : {}) },
532
315
  };
533
316
  })(),
534
317
  });
535
318
  return;
536
319
  }
537
- // Metrics (read-only): authorized by EITHER the full authToken OR a read-only metricsToken — so a
538
- // control plane (config-center) can pull metrics fleet-wide with one token, never holding each
539
- // worker's full authToken. Handled BEFORE the global gate so metricsToken-only callers aren't 401'd.
540
320
  if (req.method === "GET" && (url === "/metrics" || url === "/metrics/summary" || url === "/metrics/plan-cache")) {
541
321
  if (!deps.metrics) {
542
322
  sendError(res, 404, "feature.metrics_disabled", "metrics disabled");
@@ -544,10 +324,6 @@ export function createHttpServer(rawDeps) {
544
324
  }
545
325
  const { authToken: t, metricsToken: mt } = deps.config;
546
326
  const anyT = Boolean(t) || Object.keys(deps.config.authTokens ?? {}).length > 0;
547
- // 🔴 第一臂的 `!requirePrincipal` 是 2026-07-31 缝合审补的:同族单源 `isFleetWide` 早就带着它
548
- // (连同那段「多租户下 token 的缺席不得开放全网」的审计注释),这一处停在了旧形。要紧在于
549
- // `/metrics/plan-cache` 的 dump **按 principal 分键** ⇒ 它是租户名册而不是聚合数;而「多租 + 无
550
- // service token + 无 metrics token」并不是 boot 会拒的形(main.ts 只 warn)。dev-open 只在单用户形成立。
551
327
  const devOpen = !anyT && !mt && !deps.config.requirePrincipal;
552
328
  const ok = devOpen || (anyT && systemFor(req, deps.config) !== undefined) || (!!mt && authorized(req, mt));
553
329
  if (!ok) {
@@ -558,7 +334,6 @@ export function createHttpServer(rawDeps) {
558
334
  sendJson(res, 200, { model: deps.config.model.id, ...deps.metrics.summarize() });
559
335
  return;
560
336
  }
561
- // Per-scope objective-recurrence detail (core design/42 handoff). Same token gate as /metrics.
562
337
  if (url === "/metrics/plan-cache") {
563
338
  sendJson(res, 200, { scopes: deps.planCacheProbe?.dump() ?? {} });
564
339
  return;
@@ -567,14 +342,7 @@ export function createHttpServer(rawDeps) {
567
342
  res.end(deps.metrics.render());
568
343
  return;
569
344
  }
570
- // 鉴权桥预认证:在任何 principal-gated 路由(trace/fleet/workflows/authorizer)读取身份之【前】,
571
- // 若 Authorization bearer 形似 registry-JWT 且 verifier 在位,验它——通过则把 sub 挂进 request-local
572
- // (setSsoPrincipal),verifiedPrincipal 单一真源随后优先读它(directDoor 与 BFF 两部署同效),并同时写入
573
- // principalHeader(小写键)供 principalFrom 直读处使用。验证不通过=不设身份,后续 systemFor 因 bearer 不匹配
574
- // 任何静态 token 而 401(恒 generic,R9:错误码只进日志)。非 JWT-形 bearer(静态 token)= looksLikeJwt
575
- // 跳过,零开销,既有调用方字节不变。
576
345
  if (deps.registryJwtVerifier) {
577
- // 只认 Bearer scheme(异源复审 INFO:`Basic a.b.c` 之前会白跑一次 verify + 一条 rejected 记账噪声)。
578
346
  const authz = req.headers.authorization ?? "";
579
347
  const rawBearer = /^Bearer\s/i.test(authz) ? authz.replace(/^Bearer\s+/i, "") : "";
580
348
  if (rawBearer && looksLikeJwt(rawBearer)) {
@@ -582,41 +350,25 @@ export function createHttpServer(rawDeps) {
582
350
  if (v.ok) {
583
351
  setSsoPrincipal(req, v.identity.principal);
584
352
  req.headers[deps.config.principalHeader.toLowerCase()] = v.identity.principal;
585
- // scope claim 也挂 request-local(多租户活动租户;registry-core 0.3.0+ VerifiedIdentity.scope)。
586
- // 用途=审计归因 + 未来 scope 级 quota/过滤的读取地基。同时记一条 info 审计锚(每认证请求一条):
587
- // 此前 principal 入了 owner 但 scope 全程不落任何日志/审计(grep 实证零命中 scope)——补齐。
588
353
  if (v.identity.scope)
589
354
  setSsoScope(req, v.identity.scope);
590
355
  deps.logger?.info?.("auth_bridge_principal_accepted", { principal: v.identity.principal, ...(v.identity.scope ? { scope: v.identity.scope } : {}) });
591
356
  }
592
357
  }
593
358
  }
594
- // design/158 A9 域模块:Task Trace 只读面 + 用量统计面(routes/trace-usage.ts)。
595
- // ⚠️ 位置即契约:必须留在下面的全局 service-credential 门**之前**(traceToken-only 调用方否则先被 401)。
596
359
  if (await handleTraceUsage(req, res, url, ctx))
597
360
  return;
598
- // design/158 A9 域模块:S8 自编排 workflow 只读三路(routes/workflows.ts)。
599
- // 位置同 trace/fleet:全局 service-credential 门之前,块内 scope 门=这些读的唯一租户边界。
600
361
  if (await handleWorkflows(req, res, url, ctx))
601
362
  return;
602
- // design/158 A9 域模块:MF-Fleet 实时舰队流(routes/fleet.ts)。服务凭据门之前,按 principal 自门。
603
363
  if (await handleFleet(req, res, url, ctx))
604
364
  return;
605
- // DESIGN-269 车2 件1 域模块:A2A agent card(routes/a2a-serve.ts)。
606
- // ⚠️ 位置即契约:必须留在下面的全局 service-credential 门**之前** —— 卡是 R-11 裁定的「公网匿名
607
- // 可发现」面(挂在门后=要凭据才能发现,A2A 协商第一步就断)。它**零存量披露**(纯 config 投影,
608
- // 不读任何 task/run/session),所以匿名可读不构成披露;同域的 JSON-RPC 调用面在门**内**(下方)。
609
365
  if (await handleA2aServe(req, res, url, ctx))
610
366
  return;
611
- // Service-credential gate + SOURCE derivation: which system's token authenticated this request.
612
- // `source` is set ONLY here, from the credential — a caller cannot claim another system's identity.
613
367
  let source = null;
614
- const anyServiceAuth = hasServiceAuth(deps.config); // 单一属主(#329 的 parked 赎回席读同一句)
368
+ const anyServiceAuth = hasServiceAuth(deps.config);
615
369
  if (anyServiceAuth) {
616
370
  const sys = systemFor(req, deps.config);
617
371
  if (sys === undefined) {
618
- // 鉴权桥:静态 service-token 都不匹配 → 若入口预认证已把一个验过的 registry-JWT principal
619
- // 挂进 request-local(ssoVerifiedPrincipal),即用户直连(source="sso");否则无凭据 401。
620
372
  if (ssoVerifiedPrincipal(req)) {
621
373
  source = "sso";
622
374
  }
@@ -629,22 +381,7 @@ export function createHttpServer(rawDeps) {
629
381
  source = sys;
630
382
  }
631
383
  }
632
- // design/158 A9:`source` 的**唯一**赋值段就在上面;域模块经 ctx.req.source 读到同一个值(单向镜像,
633
- // 此行之后 handle() 不再改它)。
634
384
  reqState.source = source;
635
- // Mandatory service token on submission endpoints (center: a worker MUST validate a service-to-service
636
- // token on task creation — not "if auth is configured" — else anything in-cluster can submit & bill
637
- // directly, bypassing config-center's auth/audit). Fail-closed: the POST task/run/leader endpoints refuse
638
- // when no authToken is set, unless explicitly opted out for local dev (ALLOW_UNAUTHED_WRITES=true).
639
- // The bake door (POST /v1/images/bakes*) is build-host-RCE-capable and authed by the Bearer-token-no-cookie
640
- // model (§P2.4b) — it MUST also refuse when no service token is configured (else a forged principal header
641
- // alone could reach an operator-gated RCE door).
642
- // A-010.23 (#209 件5) adds the same sibling arm for the REWRITE doors (`isCredentialGatedRewrite`:
643
- // adoption + rules/cc-import + rules revoke) — same sentence, same threat, different verbs: one rekeys every
644
- // row a principal owns, one mints that principal's DURABLE allow rules, one deletes them. They stay OUT of
645
- // `isBillableSubmitPath` on purpose (billable=false; see that predicate's twin comment), and that family is
646
- // METHOD-AWARE (its revoke verb is DELETE) so it is evaluated OUTSIDE the POST conjunct — same shape as
647
- // `isDestructiveSessionWrite` below.
648
385
  if (!anyServiceAuth &&
649
386
  !deps.config.allowUnauthedWrites &&
650
387
  ((req.method === "POST" && (isBillableSubmitPath(url) || url.startsWith("/v1/images/bakes"))) ||
@@ -652,190 +389,99 @@ export function createHttpServer(rawDeps) {
652
389
  sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions");
653
390
  return;
654
391
  }
655
- // DRAINING — this instance is shutting down gracefully (SIGTERM received, in-flight turns being
656
- // waited on). Refuse NEW model-running work (the same billable submit set: task/stream/runs/leader + the
657
- // resume-flavored assistant verbs + approvals responds — each starts a model turn) with 503 + Retry-After so
658
- // the shell/orchestrator retries against the replacement. Everything else stays open: reads (events/health/
659
- // trace), cancel, steer/respond on in-flight runs — those help the drained turns finish, they don't start new ones.
660
392
  if (deps.drainState?.draining && req.method === "POST" && isBillableSubmitPath(url) && !isMethodDispatchedSubmitPath(url)) {
661
393
  res.setHeader("retry-after", "15");
662
- // errorCode 补齐(2026-07-28 核查:cli 的 pre-stream drain 重试只能靠 `error` 文案字面判型——
663
- // SDK 的 code 臂恒 undefined 是死码;server 改文案=对端重试静默失效)。`error:"draining"` 字面
664
- // 自此视为**冻结的 wire 契约**(旧消费端判的就是它),errorCode 是机器码正门。
665
- // #291:`reason` = 声明的停机因由(additive 键;`message` 字面冻结不动——SDK/壳按 error:"draining" 判)。
666
394
  sendError(res, 503, "draining", "draining", { message: "this instance is draining for shutdown/upgrade — retry against the replacement instance", ...(deps.drainState.reason ? { reason: deps.drainState.reason } : {}) });
667
395
  return;
668
396
  }
669
- // boot ready 门(b)(E3 workers.model 废除的 fail-closed 半场):registry 部署且无显式 env 模型时,
670
- // 首次 effective pull 落 roster 之前 worker 只有占位模型——计费提交打占位=垃圾任务,503 让编排器/壳重试
671
- // 到 roster 落地(通常 <秒)。镜像 draining 姿势(同 billable 集合;读面/cancel/steer 全开);/health 带
672
- // ready:false 加性旗。env 模型在(操作员显式声明)或非 registry 部署=恒 ready,现有部署零影响。
673
397
  if (deps.modelReady && !deps.modelReady() && req.method === "POST" && isBillableSubmitPath(url) && !isMethodDispatchedSubmitPath(url)) {
674
398
  res.setHeader("retry-after", "5");
675
399
  sendError(res, 503, "state.model_roster_pending", "model_roster_pending", { message: "this worker has no model yet (waiting for the first effective-config pull to land the roster) — retry shortly" });
676
400
  return;
677
401
  }
678
- // E6 fail-closed:对**既有会话**的破坏性写,在「无 service token + 未开 ALLOW_UNAUTHED_WRITES」
679
- // 这一形下一律拒。此形下 `sessionOwnerScopeForWrite` 判 fleet-wide ⇒ 集群内任意调用方可对任意
680
- // session 动手;policy 腿担心的是「forged principal 能 tighten/DoS 一个会话的工具」。
681
- //
682
- // 🔴 2026-08-01 复审:这里原本是**逐条路由白名单**(PUT …/policy 与 POST …/sync/import 两块),
683
- // 而 `DELETE /v1/sessions/:id`(删整条会话)**不在其中** —— 同一威胁形、爆炸半径更大却没门。
684
- // 改成族判定(`isDestructiveSessionWrite`),新增破坏性会话路由自动落进门内。
685
- // import 腿仍拦在 **Phase A**(建 staging 之前),因为族判据匹配的就是那个 URL,不是 Phase B。
686
402
  if (isDestructiveSessionWrite(req.method ?? "", url) && !anyServiceAuth && !deps.config.allowUnauthedWrites) {
687
403
  sendError(res, 503, "auth.service_token_required", "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting destructive session writes (policy / sync-import / fork / delete)");
688
404
  return;
689
405
  }
690
- // design/158 A9:能力/场景/模型目录域(routes/capabilities.ts)。位置=全局 service-credential 门之后、
691
- // 任务提交面之前,与拆分前逐行同序。
692
406
  if (await handleCapabilities(req, res, url, ctx))
693
407
  return;
694
- // design/158 A9 域模块:sideQuery 一次性问答面(routes/side-query.ts)。
695
408
  if (await handleSideQuery(req, res, url, ctx))
696
409
  return;
697
- // design/158 A9 域模块:同步提交面 POST /v1/tasks · /v1/tasks/stream(routes/tasks.ts;B3 三腿具名化在内)。
698
410
  if (await handleTasks(req, res, url, ctx))
699
411
  return;
700
- // design/158 A9 域模块:异步 run 面 + run 级动词(routes/runs.ts)。与 tasks 域共用 ctx.legs.prepareSpec。
701
412
  if (await handleRuns(req, res, url, ctx))
702
413
  return;
703
- // DESIGN-269 车2 件2 域模块:A2A JSON-RPC 端点 POST /v1/a2a(routes/a2a-serve.ts)。
704
- // ⚠️ 位置即契约:服务凭据门**之后**(R-11「持凭据才可调」),且在 drain / model-roster-pending 两道
705
- // 503 之后 —— `/v1/a2a` 在 isBillableSubmitPath 里(message/send 会烧模型)。排在 handleRuns 之后是
706
- // 因为它**复用** runs 域导出的提交腿(createDurableRun):次序上先让原生 run 面各就各位,再挂它的
707
- // 协议门面,评审读这张链时的因果方向才与实现一致。
708
414
  if (await handleA2a(req, res, url, ctx))
709
415
  return;
710
- // design/158 A9 域模块:SVC-5 workflow agent steer(routes/workflows.ts,与上面的只读三路同域)。
711
416
  if (await handleWorkflowAgentSteer(req, res, url, ctx))
712
417
  return;
713
- // design/158 A9 域模块:子代理 / 任务句柄 / 三条 HITL 应答口(routes/runs.ts,与上面的 run 面同域)。
714
418
  if (await handleRunVerbs(req, res, url, ctx))
715
419
  return;
716
- // design/158 A9 域模块:v2 leader 端点(routes/leader.ts)。
717
420
  if (await handleLeader(req, res, url, ctx))
718
421
  return;
719
- // design/158 A9:sandbox-image-pool 控制面(routes/images.ts;bakes 段在 images 段之前的顺序由域模块内部保住)。
720
422
  if (await handleImages(req, res, url, ctx))
721
423
  return;
722
- // design/158 A9 域模块:人在环审批面(durable F4 + legacy)与 assistant-scheduler 三动词
723
- // (routes/approvals-assistant.ts)。续跑一律经 ctx.legs 的 resume 家族。
724
424
  if (await handleApprovalsAssistant(req, res, url, ctx))
725
425
  return;
726
- // design/158 A9:观测只读面(routes/observability.ts):/v1/outcomes + /v1/usage。
727
426
  if (await handleObservability(req, res, url, ctx))
728
427
  return;
729
- // #154 件三:装配自证只读面(routes/diagnostics.ts):GET /v1/diagnostics/wiring(operator-only)。
730
428
  if (await handleDiagnostics(req, res, url, ctx))
731
429
  return;
732
- // design/183 §4.3:收编面(routes/adoption.ts,operator lane;POST /v1/adoption + GET /v1/adoption/:id)。
733
430
  if (await handleAdoption(req, res, url, ctx))
734
431
  return;
735
- // #270 车2:托管留存的 operator 治理面(routes/retention-ops.ts,operator lane;hold PUT/DELETE +
736
- // 审计读)。位置跟着 adoption —— 同族(部署级 operator 动作、同一条 `explicitOperatorOk` 门、
737
- // 同样 billable=false 且刻意不吃 drain / model-roster-pending 两道 503)。
738
432
  if (await handleRetention(req, res, url, ctx))
739
433
  return;
740
- // #291(C17):停机因由声明口(routes/admin-drain.ts,operator lane;POST /v1/admin/drain)。
741
- // 同族(operator 门、billable=false、不吃 drain 503——它正是 drain 前要调的口),位置跟着 retention。
742
434
  if (await handleAdminDrain(req, res, url, ctx))
743
435
  return;
744
- // #322 批1 追加件:配置手动刷新口(routes/admin-config-refresh.ts,operator lane;POST
745
- // /v1/admin/config/refresh)。与 drain 同族同门同 billable 姿势,位置紧跟它。
746
436
  if (await handleAdminConfigRefresh(req, res, url, ctx))
747
437
  return;
748
- // design/158 A9:memory 导出/同步 + /v1/policy 只读面(routes/memory-policy.ts)。
749
438
  if (await handleMemoryPolicy(req, res, url, ctx))
750
439
  return;
751
- // #264 v2-c:治理携出 bundle 的 operator 面(routes/memory-bundle.ts;POST 两口)。
752
- // 🔴 位置在 memory-policy **之后**是有意的:`/v1/memory/export` 这条路径上住着两个不同的面 ——
753
- // GET = 142-S5 的属主自助条目读(owner 门,零存在性 oracle),POST = 本件的治理包(operator-only)。
754
- // 两者按**方法**互斥,谁先谁后都不改判;把既有的 GET 面留在前面,是让新面在任何情况下都不可能
755
- // 遮住它(本模块只认 POST,一个字都不碰 GET 的匹配式)。族属上它与 adoption / retention-ops 同类:
756
- // 部署级 operator 动作、同一条 explicitOperatorOk 门、同样 billable=false。
757
440
  if (await handleMemoryBundle(req, res, url, ctx))
758
441
  return;
759
- // #154 车二:CC settings 导入两口(lane=principal,billable=false)。
760
442
  if (await handleRules(req, res, url, ctx))
761
443
  return;
762
- // design/177 共享记忆库只读面(routes/shared-memory.ts)。🔴 域头的合取式**就是**挂载条件:供给面
763
- // 与 org 折叠面双在场才有这个域,缺一个则请求落到本函数尾的全局 404(诚实缺席,不是 501)。
764
- // `capabilities.sharedMemory` 读同一个合取式 ——「说 yes ⟺ 面真能用」因此是结构性的,不靠人盯。
765
444
  if (deps.sharedMemoryStore && deps.orgMemoryDirectory && (await handleSharedMemory(req, res, url, ctx)))
766
445
  return;
767
- // design/158 A9:会话/发件链接列表面(routes/sessions-list.ts)。
768
446
  if (await handleSessionsList(req, res, url, ctx))
769
447
  return;
770
- // #A1([4147] 件3):后台 agent 名册读面(routes/agents-roster.ts)。位置跟着 sessions-list —— 同一族
771
- // (租户只读列举面、同一只 `sessionOwnerScope` 门、同样在全局服务凭据门之后)。
772
448
  if (await handleAgentRoster(req, res, url, ctx))
773
449
  return;
774
- // design/158 A9:单会话面(routes/sessions.ts:init/settings/mcp/head/events/audit/fork/delete/policy/workspace)。
775
450
  if (await handleSessions(req, res, url, ctx))
776
451
  return;
777
- // design/158 A9:2c session-sync 面(routes/session-sync.ts)。
778
452
  if (await handleSessionSync(req, res, url, ctx))
779
453
  return;
780
- // design/158 A9:D-1 通用附件面(routes/attachments.ts)。
781
454
  if (await handleAttachments(req, res, url, ctx))
782
455
  return;
783
- // design/158 A9 域模块:会话外事件注入 / 唤醒(routes/notify-wake.ts)。
784
456
  if (await handleNotifyWake(req, res, url, ctx))
785
457
  return;
786
458
  sendError(res, 404, "not_found.route", "not found");
787
459
  }
788
- /** #196:本任务跑哪只 Runner —— 由装配点按场景 hands 判别位裁定(main.ts 的 `runnerFor` 闭包)。
789
- * seam 缺席 = 单 Runner 部署形(#196 之前的行为逐字不变),不是降级臂。 */
790
460
  function runnerFor(spec) {
791
461
  return deps.runnerFor?.(spec) ?? deps.runner;
792
462
  }
793
- /** Read + validate the body, authorize, and build the spec. Returns null if it already responded.
794
- *
795
- * `bodyIn` (DESIGN-269 车2):调用方**已经**把体解析出来了(A2A 的 JSON-RPC 腿 —— 那条请求的正文是
796
- * 一次 `message/send`,腿把它翻成 `TaskRequestBody` 再进这条链)。缺席 ⇒ 照旧 `readJson(req)`。
797
- * 分歧只在**入口**:往下的每一条校验/授权/resolveSpec 都是同一段代码,这正是本参数的全部意义
798
- * (给 A2A 抄一份校验 = 抄一份会漂的 400 语义)。 */
799
463
  async function prepareSpec(req, res, bodyIn, onTypedFailure) {
800
464
  const body = bodyIn ?? (await readJson(req));
801
465
  if (!body || typeof body.objective !== "string") {
802
466
  sendError(res, 400, "request.field_invalid", "missing 'objective' string");
803
467
  return null;
804
468
  }
805
- // [920] 空毒帧第一道门:空/纯空白 objective 会成为持久历史里的空 user 帧——严格网关(anthropic/kimi)
806
- // 对空 content 400,且一帧毒全 session(同角色 merge 后残留空 block,存量救不了)。core 半场(装配
807
- // 自愈+intake 拒)在修;HTTP 面 fail-loud 是宪法「诚实优先」形:空任务无合法用例,静默吞掉才是坑。
808
469
  if (body.objective.trim().length === 0) {
809
470
  sendError(res, 400, "request.field_invalid", "objective must not be empty or whitespace-only (an empty user message poisons the session history on strict providers)");
810
471
  return null;
811
472
  }
812
- // jobId (work-view correlation): optional, but if present must be a non-empty string ≤64 chars (the column
813
- // width) — reject a malformed one up front rather than silently truncating/dropping it at the INSERT.
814
473
  if (body.jobId !== undefined && (typeof body.jobId !== "string" || body.jobId.length === 0 || body.jobId.length > 64)) {
815
474
  sendError(res, 400, "request.field_invalid", "jobId must be a non-empty string of at most 64 characters");
816
475
  return null;
817
476
  }
818
- // [#15] sessionId: a client MAY supply an ARBITRARY id (deliberate asymmetry vs fork/delete — see security.ts
819
- // `isUuidV7` doc: run-local `--session`, dev anonymous-share ids; the wire contract types sessionId as an opaque
820
- // string), but it must fit the `session_id VARCHAR(64)` column. Reject an over-long one up front rather than
821
- // 500/truncating at the INSERT (the authorizer's register() runs later in this function, after this block via the
822
- // deps.authorize() call — so the cap fires before any INSERT). LENGTH-ONLY — NOT a uuidv7 shape
823
- // check (that would break the permissive submit contract); submit stays lenient, fork/delete keep the uuidv7 gate.
824
477
  if (typeof body.sessionId === "string" && body.sessionId.length > 64) {
825
478
  sendError(res, 400, "request.field_invalid", "sessionId must be at most 64 characters");
826
479
  return null;
827
480
  }
828
- // Personalization caps: once users can edit these, an uncapped prompt/skill body is a
829
- // cost hole — reject up front, server-pinned (a UI may be stricter, never looser).
830
481
  if (typeof body.systemPrompt === "string" && body.systemPrompt.length > MAX_SYSTEM_PROMPT_CHARS) {
831
482
  sendError(res, 400, "request.field_invalid", `systemPrompt must be at most ${MAX_SYSTEM_PROMPT_CHARS} characters`);
832
483
  return null;
833
484
  }
834
- // appendSystemPrompt ([1478] R2 + codex R7 F2): fail-loud SHAPE gate on fresh submits — a defined value must
835
- // be a non-empty string within the systemPrompt cap. A non-string/empty value would be silently normalized to
836
- // "absent" by resolveSpec's defensive accept (a client serialization bug then runs the task WITHOUT its
837
- // product-knowledge block — the silent-drop class again); the cap matches systemPrompt (same prompt surface,
838
- // same per-turn cost hole). Stored resume bodies stay on the defensive drop+warn (acceptAppendSystemPrompt).
839
485
  if (body.appendSystemPrompt !== undefined) {
840
486
  if (typeof body.appendSystemPrompt !== "string" || body.appendSystemPrompt.length === 0) {
841
487
  sendError(res, 400, "request.field_invalid", "appendSystemPrompt must be a non-empty string");
@@ -846,23 +492,10 @@ export function createHttpServer(rawDeps) {
846
492
  return null;
847
493
  }
848
494
  }
849
- // codex F3/R3/R4 (capability honesty): the append-less-pack combos (rider / settings.outputStyle meeting an
850
- // already-assembled systemPrompt or a center assembled-identity pack) are adjudicated INSIDE resolveSpec —
851
- // it alone derives the effective scenario (principal ruling + defaultScenario) and holds the center-pack
852
- // snapshot the provider pick uses, so an HTTP-side probe could disagree with the actual assembly (R4:
853
- // scenario mismatch / centerPrompts refresh TOCTOU). This route passes `leg:"fresh"` below, which makes
854
- // resolveSpec 400 fail-loud (HttpError, surfaced by the catch around prepareSpec's caller); resume families
855
- // omit it and get the drop+warn mirror instead.
856
- // [#40 / TOC cwd seam] body.cwd must be an ABSOLUTE host path — fail-loud (a relative cwd would silently resolve
857
- // against the SERVICE process cwd, not the user's project — TOC review #12). Whether it is HONORED is separately
858
- // gated to the single-user host lane (resolveSpec); validating shape here is independent of that gate.
859
495
  if (body.cwd !== undefined && !isValidCwd(body.cwd)) {
860
496
  sendError(res, 400, "request.field_invalid", "cwd must be a non-empty absolute host path");
861
497
  return null;
862
498
  }
863
- // design/119 (CC --add-dir) body.additionalDirectories — fail-loud shape on submit (mirrors cwd; each entry is a
864
- // host path with the SAME absolute/no-`..` rule). Whether it is HONORED is separately gated to the host lane
865
- // (resolveSpec). An empty array is fine (no-op); a non-array or an entry failing the cwd shape is a client error.
866
499
  const addDirsRaw = body.additionalDirectories;
867
500
  if (addDirsRaw !== undefined) {
868
501
  if (!Array.isArray(addDirsRaw)) {
@@ -879,8 +512,6 @@ export function createHttpServer(rawDeps) {
879
512
  return null;
880
513
  }
881
514
  }
882
- // core 5.11.0:additionalReadDirectories(只读围栏)—— 与 additionalDirectories 同形 fail-loud
883
- // (绝对路径数组、无 `..`、同条数上限);语义差异(只进 READ 面)在 core,形校验在此对齐。
884
515
  const addReadDirsRaw = body.additionalReadDirectories;
885
516
  if (addReadDirsRaw !== undefined) {
886
517
  if (!Array.isArray(addReadDirsRaw)) {
@@ -897,9 +528,6 @@ export function createHttpServer(rawDeps) {
897
528
  return null;
898
529
  }
899
530
  }
900
- // body.settings (client SemaSettings stamp) — fail-loud shape/size caps on submit, matching the sibling
901
- // fields' posture (parseTaskSettings is also defensive on every path incl. resume; this 400s a bad submit early
902
- // rather than silently coercing — adversarial review). The semantic projection (tighten-only) happens in resolveSpec.
903
531
  const settingsRaw = body.settings;
904
532
  if (settingsRaw !== undefined && settingsRaw !== null) {
905
533
  if (typeof settingsRaw !== "object" || Array.isArray(settingsRaw)) {
@@ -911,8 +539,6 @@ export function createHttpServer(rawDeps) {
911
539
  sendError(res, 400, "request.field_invalid", `settings.outputStyle must be at most ${MAX_SETTINGS_OUTPUT_STYLE_CHARS} characters`);
912
540
  return null;
913
541
  }
914
- // codex R14: BOTH append carriers fold into ONE spec field (applyTaskSettings: `append\n\nstyle`) — two
915
- // individually-valid values can exceed the documented cap combined. One aggregate gate, same bound.
916
542
  if (typeof st.outputStyle === "string" &&
917
543
  st.outputStyle.length > 0 &&
918
544
  typeof body.appendSystemPrompt === "string" &&
@@ -921,9 +547,6 @@ export function createHttpServer(rawDeps) {
921
547
  return null;
922
548
  }
923
549
  const perms = st.permissions;
924
- // F1(#157 复审车B 留裁,裁修 2026-08-05):出现但形错(数组/标量)⇒ 400,与 settings 自身门(上方
925
- // :1097)同姿势——此前数组形被本门与 #157-③ 枚举门的 `!Array.isArray` 双双跳过、parse 层再静默
926
- // drop = 保护型配置(deny 规则)端到端无效且无声。null 保持「容器缺席」不咬(既有钉)。
927
550
  if (perms !== undefined && perms !== null && (typeof perms !== "object" || Array.isArray(perms))) {
928
551
  sendError(res, 400, "request.field_invalid", "settings.permissions must be an object");
929
552
  return null;
@@ -937,8 +560,6 @@ export function createHttpServer(rawDeps) {
937
560
  }
938
561
  }
939
562
  }
940
- // settings.env (R-survey) — fail-loud on submit, parity with outputStyle/permissions (adversarial-review LOW). The
941
- // defensive parse `cleanEnvMap` also caps these on every path (resume re-runs WITHOUT this gate), this just 400s a fresh submit.
942
563
  const envRaw = st.env;
943
564
  if (envRaw !== undefined && envRaw !== null) {
944
565
  if (typeof envRaw !== "object" || Array.isArray(envRaw)) {
@@ -957,16 +578,10 @@ export function createHttpServer(rawDeps) {
957
578
  }
958
579
  }
959
580
  }
960
- // L2 ultracode (design/111): fail-loud on a non-boolean (parity with the other settings caps; parseTaskSettings
961
- // silently ignores non-true on the resume paths the HTTP gate doesn't cover).
962
581
  if (st.ultracode !== undefined && typeof st.ultracode !== "boolean") {
963
582
  sendError(res, 400, "request.field_invalid", "settings.ultracode must be a boolean");
964
583
  return null;
965
584
  }
966
- // settings.hooks(hook-runner 阶段一)— fail-loud on submit(131/132/133 邻居姿势):一个 malformed
967
- // hooks 配置被静默丢弃 = 用户以为在生效的 deny 钩子其实没跑(安全预期破坏),必须 400。契约校验 =
968
- // registry-core 0.1.51 HooksConfig + 服务侧上限(parseHooksConfig);resume 路径由 parseTaskSettings 防御
969
- // (malformed → deferred 报告+warn)。是否 HONOR 另由单用户闸在 resolveSpec 决定(多租户=忽略非 400)。
970
585
  if (st.hooks !== undefined && st.hooks !== null) {
971
586
  const hooksParsed = parseHooksConfig(st.hooks);
972
587
  if (!hooksParsed.config) {
@@ -980,9 +595,6 @@ export function createHttpServer(rawDeps) {
980
595
  sendError(res, 400, "request.field_invalid", skillsErr);
981
596
  return null;
982
597
  }
983
- // body.images (CC parity): cap COUNT + per-image inline base64 size → 413 (the inline
984
- // `data` is otherwise passed raw to the spec; URL images are core-gated). MAX_BODY already bounds total bytes; this
985
- // adds the missing count cap (anti decode-DoS) + an explicit per-image 413. Fail-loud parity with the settings caps.
986
598
  if (Array.isArray(body.images)) {
987
599
  if (body.images.length > MAX_IMAGES_PER_REQUEST) {
988
600
  sendError(res, 413, "request.payload_too_large", `at most ${MAX_IMAGES_PER_REQUEST} images per request`);
@@ -996,10 +608,6 @@ export function createHttpServer(rawDeps) {
996
608
  }
997
609
  }
998
610
  }
999
- // design/112 C1: validate the client context shape + cap each field. The body is untrusted and timeZone/userEmail
1000
- // flow into core's env block — an object with optional string timeZone/userEmail; reject non-strings / over-long
1001
- // values (anti-DoS + anti env-block-injection bloat). core additionally validates timeZone semantically (invalid
1002
- // IANA zone → UTC, never mislabeled). locale was removed in core 1.186 (dead field) so it is not accepted here.
1003
611
  if (body.clientContext !== undefined) {
1004
612
  const cc = body.clientContext;
1005
613
  if (typeof cc !== "object" || cc === null || Array.isArray(cc)) {
@@ -1015,9 +623,6 @@ export function createHttpServer(rawDeps) {
1015
623
  return null;
1016
624
  }
1017
625
  }
1018
- // §7 P0.5 sandbox image selection: shape-validate here (a non-empty profile string; capabilitiesNeeded a
1019
- // string[]). The per-principal visibility re-admit + profile→digest resolution happen in resolveSpec (it
1020
- // holds the imageIndex + the trusted principal); a 400/404 there surfaces via the HttpError catch below.
1021
626
  if (body.sandboxImageProfile !== undefined && (typeof body.sandboxImageProfile !== "string" || body.sandboxImageProfile.length === 0 || body.sandboxImageProfile.length > 128)) {
1022
627
  sendError(res, 400, "request.field_invalid", "sandboxImageProfile must be a non-empty string of at most 128 characters");
1023
628
  return null;
@@ -1026,23 +631,14 @@ export function createHttpServer(rawDeps) {
1026
631
  sendError(res, 400, "request.field_invalid", "capabilitiesNeeded must be an array of strings");
1027
632
  return null;
1028
633
  }
1029
- // capabilitiesNeeded is a constraint ON the selected image — meaningless without a profile to select (it would
1030
- // otherwise be silently ignored while the task runs the unchecked worker-global default). Adversarial-review LOW.
1031
634
  if (Array.isArray(body.capabilitiesNeeded) && body.capabilitiesNeeded.length > 0 && (typeof body.sandboxImageProfile !== "string" || body.sandboxImageProfile.length === 0)) {
1032
635
  sendError(res, 400, "request.field_conflict", "capabilitiesNeeded requires sandboxImageProfile (it constrains the selected image)");
1033
636
  return null;
1034
637
  }
1035
- // §7 P0.5 + the multi-run orchestration gates don't compose in v1: the per-task image binds by sessionId, but
1036
- // `cascade` strips sessionId per rung and `verify` runs its verifier on a separate session — both would
1037
- // silently fall back to the worker-global default image. Reject the combo FAIL-LOUD rather than run the wrong
1038
- // image (adversarial-review round-2 MEDIUM). Lift when core propagates the image binding to those sub-runs.
1039
638
  if (typeof body.sandboxImageProfile === "string" && body.sandboxImageProfile.length > 0 && (body.cascade === true || body.verify === true)) {
1040
639
  sendError(res, 400, "request.field_conflict", "sandboxImageProfile is not supported together with cascade/verify in v1 (the per-task image would not bind to the cascade rungs / verifier sub-run)");
1041
640
  return null;
1042
641
  }
1043
- // Structured output (CC --json-schema): a plain JSON-schema OBJECT, size-capped (an uncapped schema is a
1044
- // prompt-bloat / cost hole, same posture as systemPrompt/skills). Deep schema validity is core's job — a
1045
- // malformed schema fails the task with a typed core error, not a 400 here. Threaded into spec by resolveSpec.
1046
642
  if (body.outputSchema !== undefined) {
1047
643
  if (typeof body.outputSchema !== "object" || body.outputSchema === null || Array.isArray(body.outputSchema)) {
1048
644
  sendError(res, 400, "request.field_invalid", "outputSchema must be a JSON Schema object");
@@ -1053,19 +649,10 @@ export function createHttpServer(rawDeps) {
1053
649
  return null;
1054
650
  }
1055
651
  }
1056
- // E7 (shell-host contract): reasoning-effort selection (the `/effort` picker), mapped to core's ThinkingLevel in
1057
- // resolveSpec. Validate FAIL-LOUD — a provided-but-unknown level is a 400, never silently dropped (the body.model
1058
- // silent-drop bug class). The accepted set is core's tier scale; the picker's default set is advertised on
1059
- // /v1/models (supportedEffortLevels). Absent ⇒ core uses the resolved role's default thinking.
1060
652
  if (body.reasoningEffort !== undefined && !isThinkingLevel(body.reasoningEffort)) {
1061
653
  sendError(res, 400, "request.field_invalid", "reasoningEffort must be one of: off, minimal, low, medium, high, xhigh, max");
1062
654
  return null;
1063
655
  }
1064
- // E18 (shell-host contract): resume-at handle shape. A non-empty string ≤64 (the uuidv7 eventId / entry_id column
1065
- // width). resolveSpec resolves it to a SessionTreeEntry.id (eventId→entryId anchor map) — an unknown handle 4xx's
1066
- // there. Reject combos core forbids (resumeAt + a fresh verify/cascade leg replaces/strips the session per
1067
- // rung/sub-run, so resume-at is meaningless there — fail-loud rather than silently branch a session that's about
1068
- // to be replaced), mirroring the sandboxImageProfile+cascade/verify reject.
1069
656
  if (body.resumeAt !== undefined) {
1070
657
  if (typeof body.resumeAt !== "string" || body.resumeAt.length === 0 || body.resumeAt.length > 64) {
1071
658
  sendError(res, 400, "request.field_invalid", "resumeAt must be a non-empty message eventId string (≤64 chars)");
@@ -1076,8 +663,6 @@ export function createHttpServer(rawDeps) {
1076
663
  return null;
1077
664
  }
1078
665
  }
1079
- // [833] rewind exclusive mode: two enum values only, and only riding a resumeAt (alone it qualifies nothing — a
1080
- // silent no-op is the body.model silent-drop bug class, so fail loud). Absent ⇒ core default "at" (zero regression).
1081
666
  if (body.resumeAtMode !== undefined) {
1082
667
  if (body.resumeAtMode !== "at" && body.resumeAtMode !== "before") {
1083
668
  sendError(res, 400, "request.field_invalid", 'resumeAtMode must be "at" or "before"');
@@ -1088,9 +673,6 @@ export function createHttpServer(rawDeps) {
1088
673
  return null;
1089
674
  }
1090
675
  }
1091
- // E12 (shell-host contract): prompt-suggestions opt-in. `true`/`false` or an object `{count?, role?}`. Validate
1092
- // FAIL-LOUD (a malformed value is a 400, not a silent off — the body.model silent-drop bug class); resolveSpec then
1093
- // normalizes (and re-normalizes defensively on resume, which skips this HTTP validation).
1094
676
  if (body.suggestNextPrompts !== undefined && typeof body.suggestNextPrompts !== "boolean") {
1095
677
  const s = body.suggestNextPrompts;
1096
678
  if (typeof s !== "object" || s === null || Array.isArray(s)) {
@@ -1107,33 +689,22 @@ export function createHttpServer(rawDeps) {
1107
689
  return null;
1108
690
  }
1109
691
  }
1110
- // E19 (shell-host contract): rewind-files opt-in must be a boolean (fail-loud, not silently dropped).
1111
692
  if (body.rewindFiles !== undefined && typeof body.rewindFiles !== "boolean") {
1112
693
  sendError(res, 400, "request.field_invalid", "rewindFiles must be a boolean");
1113
694
  return null;
1114
695
  }
1115
- // MF-30 memory PAUSE (option B): the per-request write toggle must be a boolean (fail-loud — a malformed value
1116
- // silently writing to memory when the user asked to pause is exactly the silent-drop bug class).
1117
696
  if (body.memoryWrite !== undefined && typeof body.memoryWrite !== "boolean") {
1118
697
  sendError(res, 400, "request.field_invalid", "memoryWrite must be a boolean (false = pause memory writes for this run)");
1119
698
  return null;
1120
699
  }
1121
- // design/114 Phase3: requireExistingSession opt-in must be a boolean (fail-loud — a truthy non-boolean silently
1122
- // NOT triggering the fail-loud is the very "silent fresh run" this flag exists to prevent).
1123
700
  if (body.requireExistingSession !== undefined && typeof body.requireExistingSession !== "boolean") {
1124
701
  sendError(res, 400, "request.field_invalid", "requireExistingSession must be a boolean");
1125
702
  return null;
1126
703
  }
1127
- // §4 (Fork): enableFork opt-in must be a boolean (fail-loud, not silently dropped — mirrors selfOrchestration).
1128
704
  if (body.enableFork !== undefined && typeof body.enableFork !== "boolean") {
1129
705
  sendError(res, 400, "request.field_invalid", "enableFork must be a boolean");
1130
706
  return null;
1131
707
  }
1132
- // design/131/132/133 (1.127.0): fresh-submit shape validation for the three new opt-ins — a
1133
- // malformed value must 400 here, not silently normalize to "off" (the body.model silent-drop bug class).
1134
- // resolveSpec's normalizers stay defensive for the RESUME path (which re-enters without HTTP validation).
1135
- // NOTE: `resilience.bypassBreaker=true` from a NON-operator is a shape-valid permission downgrade (dropped
1136
- // by the normalizer, not a 400) — the field's authz, not its type.
1137
708
  if (body.resilience !== undefined) {
1138
709
  const r = body.resilience;
1139
710
  if (typeof r !== "object" || r === null || Array.isArray(r)) {
@@ -1152,28 +723,16 @@ export function createHttpServer(rawDeps) {
1152
723
  sendError(res, 400, "request.field_invalid", "finalVerification must be a boolean");
1153
724
  return null;
1154
725
  }
1155
- // [854]④: fresh-submit shape validation for per-request 配速 —— 已知三键必须是正整数,否则 400 fail-loud
1156
- // (body.model silent-drop 同类教训:静默 normalize 成「没配速」正是本件要防的假成功)。0 也拒绝:core 侧
1157
- // `timeoutSec > 0` 才生效(0=不设墙,会绕过 TASK_TIMEOUT_MAX_SEC 封顶),maxTurns/maxOutputTokens 的 0 无意义。
1158
- // 枚举外键与 resilience/attachments 同口径:容忍(只校验已知键)——但退役键与预算族错位键**点名 400**
1159
- // (它们是「caller 有明确意图、本层却不会采纳」的已知键形,容忍=静默吞意图)。resolveSpec 的
1160
- // normalizeLimits 对 RESUME 重放路径保持 defensive(按键 DROP,不 throw)。
1161
726
  if (body.limits !== undefined) {
1162
727
  const l = body.limits;
1163
728
  if (typeof l !== "object" || l === null || Array.isArray(l)) {
1164
729
  sendError(res, 400, "request.field_invalid", "limits must be an object { maxWalltimeMs?, maxOutputTokens?, maxTurns? } (positive integers)");
1165
730
  return null;
1166
731
  }
1167
- // core 5.8.0 时限重构:旧键响亮拒并点名替代(core 同姿势 config.limit_unknown_key;「勿自行兼容」是
1168
- // core 的公开指令)。timeoutSec(秒)→ maxWalltimeMs(毫秒,注意单位);deadline 族三 opt-out 随
1169
- // 机制整族退役(nudge/call-cap/graceful-finalize 已从引擎移除),无替代键。fresh fail-loud;resume
1170
- // 重放不过此门,resolveTaskLimits 对旧键 defensive DROP + 具名留痕(task_limits_legacy_key_dropped:
1171
- // 存量 suspended 体降级为默认配速——单用户无 env 墙时=整任务无墙,多租=延到租户墙,非「方向安全」)。
1172
732
  if (l.timeoutSec !== undefined) {
1173
733
  sendError(res, 400, "request.field_invalid", "limits.timeoutSec was retired in core 5.8.0 — use limits.maxWalltimeMs (milliseconds; opt-in wall clock)");
1174
734
  return null;
1175
735
  }
1176
- // 166-T3(靶4b):deadlineNudge 的替代=approachNotice(core RETIRED 表同源);另两键随机制退役无替代。
1177
736
  if (l.deadlineNudge !== undefined) {
1178
737
  sendError(res, 400, "request.field_invalid", "limits.deadlineNudge was retired in core 5.8.0 — use limits.approachNotice (default on at 80%/95%; false turns it off, {at: [r1, r2]} tunes thresholds)");
1179
738
  return null;
@@ -1184,11 +743,6 @@ export function createHttpServer(rawDeps) {
1184
743
  return null;
1185
744
  }
1186
745
  }
1187
- // 预算族错位键(core 5.8.0「预算入 limits」的单向缺口):caller 按 core 键形写 limits.maxCostUsd/
1188
- // maxTokens/degrade/budgetStreamCancel 时,本服务的采纳面在 body **顶层**(resolve-spec.ts
1189
- // cappedCeiling)/运营方 env(MODEL_DEGRADE_*)/core 默认(budgetStreamCancel=maxCostUsd 在场即
1190
- // true)——不点名拒则被静默吞掉,方向「更松」(caller 以为封了 0.5 刀,实际无成本上限)。
1191
- // 退役键同姿势 fail-loud;resume 重放不过此门,normalizeLimits 对这四键维持 defensive DROP。
1192
746
  for (const k of ["maxCostUsd", "maxTokens"]) {
1193
747
  if (l[k] !== undefined) {
1194
748
  sendError(res, 400, "request.field_invalid", `limits.${k} is not read from limits here — send it at the request body top level (${k}); it is capped by the operator ceiling ${k === "maxCostUsd" ? "MAX_TASK_COST_USD" : "MAX_TASK_TOKENS"}`);
@@ -1210,7 +764,6 @@ export function createHttpServer(rawDeps) {
1210
764
  return null;
1211
765
  }
1212
766
  }
1213
- // 166-T3:approachNotice fresh-submit fail-loud(判据单源 normalizeApproachNotice,阈值规则对齐 core a<=b)。
1214
767
  {
1215
768
  const v = l.approachNotice;
1216
769
  if (v !== undefined && normalizeApproachNotice(v) === undefined) {
@@ -1219,11 +772,6 @@ export function createHttpServer(rawDeps) {
1219
772
  }
1220
773
  }
1221
774
  }
1222
- // [876] per-task subagents(core 1.295 TaskSpec.agents):fresh-submit 白名单门 fail-loud。与 resilience/
1223
- // limits 的「容忍未知键」取舍相反 —— agents 是能力定义,静默丢字段=定义被悄悄削(丢一个 denyTools/
1224
- // isolation 就是放宽),400 点名未知键更安全。permissionMode 有意不纳(core 拍,独立批)——出现按未知键拒。
1225
- // 形状细则(name 必填且数组内唯一/各字段浅形状/≤32 条)单源在 spec-fields.ts validateTaskAgents;RESUME
1226
- // 重放不过这道门 → resolveSpec 侧 taskAgentsFromBody defensive(非法项按条 DROP+warn,不砖 resume)。
1227
775
  if (body.agents !== undefined) {
1228
776
  const agentsErr = validateTaskAgents(body.agents);
1229
777
  if (agentsErr) {
@@ -1231,27 +779,18 @@ export function createHttpServer(rawDeps) {
1231
779
  return null;
1232
780
  }
1233
781
  }
1234
- // [922]①/[915]② interactiveTools(core 1.296 三态旋钮):boolean fail-loud 同邻居姿势。per-run 工具面
1235
- // 选择(AskUserQuestion/present_plan 挂载判据覆写),无跨租户面 ⇒ 不设租户门;undefined=core 自动判据。
1236
782
  if (body.interactiveTools !== undefined && typeof body.interactiveTools !== "boolean") {
1237
783
  sendError(res, 400, "request.field_invalid", "interactiveTools must be a boolean");
1238
784
  return null;
1239
785
  }
1240
- // [1909]⑧ oneShot(core 5.23.0):boolean fail-loud,与它的姊妹旋钮 interactiveTools 逐字同姿势 ——
1241
- // 两者说的是同一件事的两半(这次提交是不是 `-p` 一次性形)。非 boolean 静默丢会让调用方以为已经声明
1242
- // 了一次性、模型却照旧被告知「结束回合等通知」= 后台结果丢失,正是本键存在的那个失败形。
1243
786
  if (body.oneShot !== undefined && typeof body.oneShot !== "boolean") {
1244
787
  sendError(res, 400, "request.field_invalid", "oneShot must be a boolean");
1245
788
  return null;
1246
789
  }
1247
- // [854]② retainBackgroundProcesses:boolean fail-loud(邻居姿势 —— 非 boolean 静默变「不驻留」正是
1248
- // silent-drop 病灶);单用户闸/多租忽略在 resolveSpec(retainBackgroundProcessesFromBody)。
1249
790
  if (body.retainBackgroundProcesses !== undefined && typeof body.retainBackgroundProcesses !== "boolean") {
1250
791
  sendError(res, 400, "request.field_invalid", "retainBackgroundProcesses must be a boolean");
1251
792
  return null;
1252
793
  }
1253
- // [1052]② excludeTools/deferTools:数组-of-非空-string fail-loud(邻居姿势——坏形静默丢=工具面照旧,
1254
- // 调用方以为收窄了却没收=silent-drop 病灶同族)。per-run 收窄无跨租户面 ⇒ 无租户门(interactiveTools 同)。
1255
794
  for (const key of ["excludeTools", "deferTools"]) {
1256
795
  const v = body[key];
1257
796
  if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
@@ -1259,7 +798,6 @@ export function createHttpServer(rawDeps) {
1259
798
  return null;
1260
799
  }
1261
800
  }
1262
- // [1144]/[1146] promptProfile:枚举 fail-loud(坏值静默丢=调用方以为切了形没切,silent-drop 病灶同族)。
1263
801
  {
1264
802
  const v = body.promptProfile;
1265
803
  if (v !== undefined && v !== "simple" && v !== "classic") {
@@ -1267,8 +805,6 @@ export function createHttpServer(rawDeps) {
1267
805
  return null;
1268
806
  }
1269
807
  }
1270
- // [2856]② toolMaterializeStrategy:枚举 fail-loud(邻居 promptProfile 同款,同一条理由——静默折缺省
1271
- // 会让调用方以为切了策略却没切,而这个键的整个存在理由就是 #181 那条「缺省下无界循环」)。
1272
808
  {
1273
809
  const v = body.toolMaterializeStrategy;
1274
810
  if (v !== undefined && v !== "static" && v !== "swap") {
@@ -1276,11 +812,6 @@ export function createHttpServer(rawDeps) {
1276
812
  return null;
1277
813
  }
1278
814
  }
1279
- // #157-② permissionMode:枚举 fail-loud(邻居 promptProfile 同款)。此前开放 string + coercePermissionMode
1280
- // 未知词静默折 "default"(最爱问档)——方向对(收紧)但静默:拼错(大小写 "Plan")的调用方以为进了 plan
1281
- // 模式,实拿恒询问,silent-drop 病灶同族([2759] cli settings.json 同源病)。RESUME 重放持久化 body 不过
1282
- // 此门(coercePermissionMode 宽容层护存量,双层与 [854]/outputStyle 同姿势);bundle 的
1283
- // settings.permissions.defaultMode drop 臂本批不动(候 [2759] 跨仓对表统一裁,task-settings.ts:216 注)。
1284
815
  {
1285
816
  const v = body.permissionMode;
1286
817
  if (v !== undefined && v !== "default" && v !== "acceptEdits" && v !== "plan" && v !== "bypassPermissions" && v !== "auto") {
@@ -1288,10 +819,6 @@ export function createHttpServer(rawDeps) {
1288
819
  return null;
1289
820
  }
1290
821
  }
1291
- // #157-③([2766] cli 对表裁决):bundle 的 settings.permissions.defaultMode 与顶层同形 400 —— 经壳的
1292
- // 流量上 wire 前已过闭集校验(壳侧对人手编辑面走「降级+响亮」b979f74),server 收到未知词只可能来自
1293
- // 旧版壳或第三方直连=程序化入口,严格拒合理。parseTaskSettings 的 drop 臂保留护 RESUME 存量
1294
- // (task-settings.ts:216 双层,同 permissionMode/outputStyle 姿势)。
1295
822
  {
1296
823
  const s = body.settings;
1297
824
  if (s !== null && typeof s === "object" && !Array.isArray(s)) {
@@ -1305,30 +832,18 @@ export function createHttpServer(rawDeps) {
1305
832
  }
1306
833
  }
1307
834
  }
1308
- // [865]② fresh-submit 未知模型 400 fail-loud(clay 生产实锤:未知 body.model 静默落 default = 用户不知情
1309
- // 换模型)。🔴 门目录必须与 resolveSpec 同源 = expandTiers 增广视图(codex H1:裸 config.models 会把合法
1310
- // 档位词 "pro"/CC alias "sonnet" 当未知 400 误杀——SDK 契约明确允许它们;增广后 matchCatalogModel 对
1311
- // name/tier 词/alias/id 四形一体裁决)。restart 窗口注记与 main.ts wireCatalog 同款:tiers 热刷新 vs
1312
- // Runner 构造期快照,门短暂超前=fail-loud 而非静默降级,诚实。"default" 是合法目录键(hasOwn 命中)。
1313
- // RESUME 重放持久化 body 不过这道门(模型事后被移出目录时砖死 resume 比降级糟)——resolveSpec 侧对未知
1314
- // ref warn 降级,双层与 [854] limits 同姿势。settings.model 保持 lenient(文档化,picker 路径 =
1315
- // body.model 才是本病灶面)。
1316
835
  if (body.model !== undefined) {
1317
836
  if (typeof body.model !== "string" || body.model.length === 0) {
1318
837
  sendError(res, 400, "request.field_invalid", "model must be a non-empty string (a configured catalog name, tier word, or model id)");
1319
838
  return null;
1320
839
  }
1321
- const bare = deps.config.models ?? {}; // 防御:极简 stub deps 可无 models 域(真 loadConfig 恒有)
840
+ const bare = deps.config.models ?? {};
1322
841
  const catalog = expandTiers(bare, deps.config.tiers ?? {}) ?? bare;
1323
842
  if (matchCatalogModel(body.model, catalog) === undefined) {
1324
843
  sendError(res, 400, "request.unknown_reference", `unknown model "${body.model.slice(0, 120)}" — not in the configured catalog (name, tier word, or id)`, { available: mentionableNames(deps.config, catalog) });
1325
844
  return null;
1326
845
  }
1327
846
  }
1328
- // [1479]① compactionModel — the SAME fail-loud posture + gate catalog as body.model (an unknown cheap-gear pick
1329
- // silently burning the MAIN model's price is exactly the silent-drop bug class this train exists to kill).
1330
- // RESUME replays skip this gate (resolveSpec drops+warns there — core's resolveModel throws on an unknown ref,
1331
- // which would brick the resume).
1332
847
  if (body.compactionModel !== undefined) {
1333
848
  if (typeof body.compactionModel !== "string" || body.compactionModel.length === 0) {
1334
849
  sendError(res, 400, "request.field_invalid", "compactionModel must be a non-empty string (a configured catalog name, tier word, or model id)");
@@ -1371,17 +886,12 @@ export function createHttpServer(rawDeps) {
1371
886
  }
1372
887
  }
1373
888
  }
1374
- // 历史复审轴A #3(1.254):verify 环补 cost 顶——cascade 腿一直映 costCeilingMicroUsd,verify 腿漏了
1375
- // (每轮独立 runTask,总花费此前仅受 maxRounds 约束);同源 body.maxCostUsd,同换算。
1376
889
  const verify = verifyRoundsFromBody(body);
1377
890
  const cascade = body.cascade === true;
1378
891
  if (cascade && verify) {
1379
892
  sendError(res, 400, "request.field_conflict", "verify and cascade are mutually exclusive (verify = adversarial gate; cascade = cheap→strong ladder)");
1380
893
  return null;
1381
894
  }
1382
- // E12: suggestNextPrompts is meaningless on the verify/cascade wrappers — they return a TaskResult, not
1383
- // a stream with core's post-completion suggestions() pass, so the suggestion event would never be produced. Reject
1384
- // the combo fail-loud rather than silently accept-and-drop it (mirrors the resumeAt + verify/cascade reject above).
1385
895
  if (body.suggestNextPrompts && (verify || cascade)) {
1386
896
  sendError(res, 400, "request.field_conflict", "suggestNextPrompts is not supported with verify or cascade (those return a result, not a streamed run)");
1387
897
  return null;
@@ -1393,88 +903,26 @@ export function createHttpServer(rawDeps) {
1393
903
  try {
1394
904
  const auth = deps.authorize ? await deps.authorize({ req, body }) : undefined;
1395
905
  const spec = await deps.resolveSpec(body, req, auth, { leg: "fresh" });
1396
- // #196:执行 Runner 与 spec 同一次解析产出 —— 两个消费域(tasks 同步/流式、runs 异步)拿的都是这一只,
1397
- // 不各自再问一遍(再问 = 两处判别,正是漂移的成因)。
1398
906
  return { spec, runner: runnerFor(spec), auth, verify, cascade, jobId: body.jobId, body };
1399
907
  }
1400
908
  catch (err) {
1401
909
  if (err instanceof HttpError) {
1402
- // 🔴 codex 轮2 R2-4(验真后修):`onTypedFailure` 在场 ⇒ **不写响应**,把 typed 失败交回调用方。
1403
- // 为什么必须有这个口:本仓的 typed 拒绝会把结构化 `extra` 一起回显(`scenario_unknown` 的
1404
- // extra 是**本部署的全部场景名**)。那对壳/SDK 是指路材料,对一个 **A2A 外部 peer** 却是内部
1405
- // 词表泄漏 —— 而 DESIGN-269 车2 的整条 skill 声明制,存在的理由就是不把场景名给公网。
1406
- // 加钩子而不是重构响应形:三个既有调用方一字不传 ⇒ 行为逐字节不变(仍是这一行 sendError)。
1407
910
  if (onTypedFailure) {
1408
911
  onTypedFailure(err);
1409
912
  return null;
1410
913
  }
1411
- // Typed rejection: echo the stable `code` + any structured extras (e.g. the scenario
1412
- // allowlist) beside the human message — machine code on the wire, prose stays with the shell/web.
1413
914
  sendError(res, err.status, httpErrorCode(err.status, err.code), err.message, { ...(err.extra ?? {}) });
1414
915
  return null;
1415
916
  }
1416
917
  throw err;
1417
918
  }
1418
919
  }
1419
- // design/158 A9:`DecideBinding` 已上提到 `http/wire-types.ts` 叶子(approvals 域模块与 resume 腿同名一形),
1420
- // 本处仅留指路;形状逐字未变。
1421
- /**
1422
- * The VERIFIED fleet scope for a resume publish (TOC review #6 parity with the create paths). Prefer the
1423
- * crypto-verified `gatedPrincipal(req)` — the SAME identity GET /v1/fleet/stream filters by — on a real HTTP
1424
- * door; fall back to the checkpoint's own scope (`auth.principal`) only when there is NO request (the internal
1425
- * D-D SLA deny-sweep, which has no caller to verify). NEVER the spoofable `auth.principal` on a live door:
1426
- * a spoofed header would re-publish the resumed row under a victim's scope or under "default" (vanishing from
1427
- * its real owner's stream). Mirrors the sync/create sites' `gatedPrincipal(req, deps.config) ?? "default"`.
1428
- *
1429
- * ✅ FIDELITY EDGE — CLOSED by the F-fix (2026-07-01, direct-door principal spoofing). On the REQ-LESS leg (the
1430
- * internal D-D SLA deny-sweep, `req===undefined`) we fall back to `auth.principal` = the checkpoint's stored scope.
1431
- * Since the F-fix, createAuthorizer derives `auth.principal` (hence spec.principal, hence the checkpoint scope core
1432
- * persists at suspend) from `verifiedPrincipal` — the VERIFIED JWT sub on a direct door, the SAME identity the
1433
- * create path's `gatedPrincipal` tags the row with. So the earlier caveat — a misconfigured direct door where the
1434
- * raw `x-agent-principal` ≠ the verified sub could surface a swept row under the wrong tenant's /v1/fleet/stream —
1435
- * is GONE: the checkpoint scope is now always the verified identity, identical to what the create path tagged. The
1436
- * "TRUE fix" once deferred here (persist the verified principal on the checkpoint at create) is exactly what the
1437
- * F-fix did upstream at createAuthorizer; the req-less fallback and the create-path scope now agree on every posture.
1438
- */
1439
920
  const resumeFleetScope = (req, auth) => req ? (gatedPrincipal(req, deps.config) ?? "default") : (auth.principal ?? "default");
1440
- /**
1441
- * Durable F4 resume (design/45): map a session → its pending checkpoint token INTERNALLY,
1442
- * rebuild the taskConfig via the same `resolveSpec` path (from the sessionId-keyed checkpoint_ctx), then
1443
- * `runner.resume`. The capability token never leaves the service. Updates the parked run row to its new state.
1444
- */
1445
- async function resumeCheckpoint(sessionId, decision, reason,
1446
- // core `ApprovalSettledBy`(5.23.0):**这次结算的出处**,由调用方命名。REQUIRED —— core 的字段
1447
- // 文档把义务钉在结算点上(「Fill it at every settlement site」),并说明集中推导正是「窗到期被报成
1448
- // 另一个人拒绝」的成因。本函数有两个调用方且函数内**无从分辨**(sweep 与人为 decide 在这里唯一
1449
- // 的差别是 `req` 缺席,那是「有没有 HTTP 请求」不是「谁结算的」),所以只能穿参。
1450
- // ⚠️ core 的相容规则:`decision:"approve"` 只接受 `"human"` 或缺席 —— 「没人答所以它跑了」不是
1451
- // 任何事实的记录。今天唯一传非 `"human"` 的调用方(SLA sweep)恒 deny,故不冲突;将来若有一个
1452
- // 会 approve 的非人调用方,它该传的是**缺席**而不是一个自造的词。
1453
- settledBy, req, // absent on the internal D-D SLA deny-sweep (resolveSpec rebuilds auth from the checkpoint, not req)
1454
- answer, binding, onResumeCommitted, // codex M2: fires post-markResuming-CAS, pre-model-drive (decide leg's exemption grant); MUST NOT throw the resume down — callers swallow internally. overrideSessionId = parked 腿的 root-会话 grant 锚([1591] 候裁②),legacy 腿不传。
1455
- // #316:200 受理语义(HTTP `/decide` 传 true;内部 D-D SLA deny-sweep 不传 —— 没有在等响应的人)。
1456
- // 语义与判据的唯一属主 = `DriveResumeArgs.acceptEarly` 顶注。
1457
- acceptEarly) {
921
+ async function resumeCheckpoint(sessionId, decision, reason, settledBy, req, answer, binding, onResumeCommitted, acceptEarly) {
1458
922
  const cs = deps.checkpointStore;
1459
- // No scope filter here BY DESIGN (council): an operator may view/decide ANY tenant's pending approval.
1460
- // This load-before-CAS only READS; the definitive multi-tenant guard is the
1461
- // `AND scope = ?` inside the atomic resolve CAS — a wrong-scope decision can never EXECUTE.
1462
923
  const token = await cs.findPendingTokenBySession(sessionId);
1463
- // D-1 approval_stale (opt-in, only when the operator echoed the checkpointToken they saw): if it no longer
1464
- // matches the current pending — resolved, superseded by a re-mint, or expired — they are deciding a STALE
1465
- // view. Reject distinctly from the markResuming session-CAS 409 below: the SDK keys ApprovalStaleError on
1466
- // errorCode:"approval_stale" while the CAS 409 stays bare (→ ConflictError), so the two 409s never collide.
1467
924
  if (binding?.checkpointToken && binding.checkpointToken !== token) {
1468
925
  const seen = await cs.get(binding.checkpointToken).catch(() => null);
1469
- // 🔴 `terminal` 只在该 token **确实属于本会话**时披露(2026-07-26 修)。
1470
- // 此前是 `seen ? "resolved" : "expired_abort"` —— 而这次 `cs.get` 用的是**调用方给的** token 且
1471
- // 存储层**没有 scope 谓词**(`WHERE token = ?`,连 status 都不过滤),这一步还跑在下面那个
1472
- // `if (!token) → 404` **之前**。于是任何持凭据者都能拿一串 token 来问「它在库里吗」:
1473
- // 命中 ⇒ `terminal:"resolved"`,不命中 ⇒ `terminal:"expired_abort"` —— **一位跨租户存在性 oracle**,
1474
- // 与本文件自述的「no existence oracle」相反。token 本身不可猜(uuid 级),所以这不是枚举面;
1475
- // 真实伤害是**泄漏出去的 token 可被免费确认有效性**(从日志/截图/工单捡到一串,不必也不能用它 resume,
1476
- // 却能确认它对应一条真实 checkpoint 行、以及它是否已被解决)。
1477
- // 归属不上就**省略这个键**(SDK 侧 `terminal?:` 本来可选)——不撒谎、也不泄露;真实原因照旧进日志。
1478
926
  const attributable = seen !== null && seen.sessionId === sessionId;
1479
927
  if (!attributable && seen !== null) {
1480
928
  deps.logger?.warn?.("approval_stale_token_foreign", { sessionId, tokenSession: seen.sessionId ?? null });
@@ -1489,47 +937,23 @@ export function createHttpServer(rawDeps) {
1489
937
  };
1490
938
  }
1491
939
  if (!token) {
1492
- // 🔴 文案与 owner 门那条统一(2026-07-26 修)。此前两句可区分:owner 门拒 ⇒ "approval not found";
1493
- // 本会话**没有** pending ⇒ 这条更详细的。而 `peekPendingScope` 在没有 pending 时返回 `null`、`null` 被当作
1494
- // "开放"放过门 ⇒ 两句文案精确区分「这儿有**别人的** pending」与「这儿没有 pending」。
1495
- // 后果:任何持凭据的租户可逐个 sessionId 判定「此刻有一条别人的待决审批」,拼出一张跨租户的
1496
- // 「谁正卡在审批上」活动图(时序/规模情报)—— 而本文件自述承诺的正是不给这个。
1497
- // 统一成同一句;**诊断不丢**,只是挪进日志(部署方有日志权限,外部调用方没有)。
1498
940
  deps.logger?.info?.("approval_decide_no_pending", { sessionId });
1499
941
  return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
1500
942
  }
1501
- // get (checkpoint table) + getCtx (checkpoint_ctx table) are independent — load concurrently (council).
1502
- const cpPromise = cs.get(token); // throws if the checkpoint format is newer than this build (version guard)
943
+ const cpPromise = cs.get(token);
1503
944
  const ctxPromise = cs.getCtx(sessionId);
1504
945
  let cp;
1505
946
  try {
1506
947
  cp = await cpPromise;
1507
948
  }
1508
949
  catch (e) {
1509
- void ctxPromise.catch(() => { }); // don't leak an unhandled rejection from the parallel load
950
+ void ctxPromise.catch(() => { });
1510
951
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
1511
952
  }
1512
953
  if (!cp)
1513
954
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
1514
- // 🔴 GATE-KIND GUARD ([1995]①,`gate_not_resumable` / `gate_not_plan_review` 的**反方向镜像**):这条腿
1515
- // 只解 TOOL 审批门(core gateMatch:`human` / `irreversible_ask` ↔ outcome.gate "policy_ask")。此前它无
1516
- // 条件铸 outcome{gate:"policy_ask"} 交给 core —— 一个 plan_review / resource_limit / task_done park 被误
1517
- // 路由到 /decide(而 /v1/approvals 把它们与工具审批**混在同一个队列**里、toolName 同为 null)就会深入到
1518
- // core 的 `checkpoint.gate_mismatch`。那是 core 的 **pre-CAS** 拒绝(checkpoint 仍 pending),但 server 的
1519
- // CheckpointError 分类表把 gate_mismatch 归为 TERMINAL ⇒ 行被 setTerminal 成 failed + 释放 task_active,
1520
- // 于是「行说这活死了、park 却还活着」:claim 泄漏(同 session 可并发起第二个 run)、事后用正确的腿救能
1521
- // 200 真跑完却拿不到 taskId、run 行永停 failed(账本与现实分叉)。这里 pre-CAS 纯拒绝:不动 checkpoint、
1522
- // 不动 run 行、不烧模型腿,与另外两条腿的守卫同姿势(core 的 gate-match 仍是 fail-closed 兜底)。
1523
- // ⚠️ 门放在 parked 赎回腿**之前**:赎回腿同样只铸 policy_ask outcome,错门 cp 在那条链上只会更深地炸。
1524
- // D-D SLA deny-sweep 不受影响:它的取行查询本就只选 gate_kind ∈ APPROVAL_GATE_KINDS(tool-approval.ts 单一属主)。
1525
955
  const decideGateKind = cp.gate?.kind;
1526
- if (!isApprovalGateKind(decideGateKind)) { // A-002.1 单一属主
1527
- // [2400] HITL-6(clay 裁 2026-08-03,不做兼容):补机读指路——与 [2255]① 的 409 材料同形
1528
- // (pendingGate{kind, decidePath},resumeEntryForGate 单源表),客户端凭 decidePath 直达对的
1529
- // resume 入口,不再解析人话文案。taskId 源=账本活行(getActiveTaskId,与 active-run-conflict.ts
1530
- // 的 409 材料同源)——cp.sourceTaskId 是 core 契约里 issuing worker 的 **session id**(ECHO-ONLY
1531
- // triage,checkpoint-store.d.ts),拿它当 run taskId 铸路径=打出去必 404([3683]-1 同病族,
1532
- // 扫描 P1 修)。账本无活行 ⇒ 只带 kind,诚实不铸假路径。
956
+ if (!isApprovalGateKind(decideGateKind)) {
1533
957
  const activeForPath = decideGateKind
1534
958
  ? await deps.runStore?.getActiveTaskId(sessionId).catch(() => undefined)
1535
959
  : undefined;
@@ -1545,10 +969,6 @@ export function createHttpServer(rawDeps) {
1545
969
  },
1546
970
  };
1547
971
  }
1548
- // [2400] HITL-12(clay 裁 2026-08-03:不兼容老客户端):HTTP 腿强制决策-动作绑定——无 checkpointToken
1549
- // 且无 boundCallId 的 decide 拒收(此前静默回落 current-pending:并发窗口下 A 想批的门刚被 B 的新门顶掉
1550
- // 就裁错对象)。内部 D-D SLA deny-sweep(req 缺席)豁免:系统腿 deny 过期行,无并发歧义。位置刻意在
1551
- // gate-kind 守卫**之后**:错门型先得 409+pendingGate 指路(HITL-6),而不是被本门的 400 遮蔽。
1552
972
  if (req !== undefined && !binding?.checkpointToken && !binding?.boundCallId) {
1553
973
  return {
1554
974
  status: 400,
@@ -1559,12 +979,6 @@ export function createHttpServer(rawDeps) {
1559
979
  },
1560
980
  };
1561
981
  }
1562
- // design/153 件3d parked 赎回腿([1574]/[1584]/[1588] core 裁定;src/parked-decide.ts):该 pending
1563
- // checkpoint 若属于一个 parked 后台子代,decide 必须走 claim→裸 execute(reviveClaim) 链——legacy 腿的
1564
- // resumeStream 会绕开 bg registry(无 consumeParkedFlip、行永 parked、生命周期分叉),故 parked-first。
1565
- // 判别 miss(undefined)= 任务级 suspend,原样落到下方 legacy 腿(零回归)。⚠️ 门在 `req !== undefined`:
1566
- // 内部 D-D SLA deny-sweep(req 缺席)对 parked 子代 cp 保持现状(getCtx null → 409 跳过本 tick)——
1567
- // 过期 parked 的收割属 reaper 的 expire+reconcileParkedAgents 车道,sweep 不做重活赎回。
1568
982
  if (req !== undefined && deps.backgroundAgentStore !== undefined && deps.parkedReviveTool !== undefined) {
1569
983
  const parked = await decideParkedAgent({
1570
984
  agentStore: deps.backgroundAgentStore,
@@ -1576,24 +990,16 @@ export function createHttpServer(rawDeps) {
1576
990
  ...(deps.logger ? { warn: (event, fields) => deps.logger?.warn?.(event, fields) } : {}),
1577
991
  }, {
1578
992
  token, scope: cp.scope, sessionId: cp.sessionId, pendingAction: cp.pendingAction,
1579
- // #204 件6③:赎回腿是**另一个**结算点,出处同样穿参(它自己不猜)。今天走到这里的恒是人为
1580
- // /decide —— 上面那道门写着 `req !== undefined`,而 sweep 恰是 req 缺席的那条腿。
1581
993
  decision, settledBy, ...(reason !== undefined ? { reason } : {}),
1582
994
  ...(binding !== undefined ? { binding: { ...(binding.boundCallId !== undefined ? { boundCallId: binding.boundCallId } : {}), ...(binding.boundInputHash !== undefined ? { boundInputHash: binding.boundInputHash } : {}), ...(binding.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}) } } : {}),
1583
- // remember 的 grant 闭包(onResumeCommitted 的唯一现役来源)在 parked 腿以行的 root/host
1584
- // 会话为锚透传([1591] 候裁② server 修——落子代会话则探针键永不相交,见 grantOnCommit 注);
1585
- // answer 原样透传(RB-459,core 5.7.0 起 revive 侧有挂点;形状校验已在路由层 isQuestionAnswer)。
1586
995
  ...(onResumeCommitted ? { grantRemember: (rootSessionId) => onResumeCommitted(rootSessionId) } : {}),
1587
996
  ...(answer !== undefined ? { answer } : {}),
1588
997
  });
1589
998
  if (parked !== undefined) {
1590
- void ctxPromise.catch(() => { }); // 并行 ctx 读不再被消费,吞掉可能的 rejection
999
+ void ctxPromise.catch(() => { });
1591
1000
  return parked;
1592
1001
  }
1593
1002
  }
1594
- // Guard the ctx await too (council): if cpPromise resolved but ctxPromise REJECTS (a TiDB blip on the
1595
- // checkpoint_ctx read), an unguarded await would escape to the generic 500 — inconsistent with the 409 the
1596
- // cpPromise branch returns for the same class of transient load failure.
1597
1003
  let ctx;
1598
1004
  try {
1599
1005
  ctx = await ctxPromise;
@@ -1603,18 +1009,7 @@ export function createHttpServer(rawDeps) {
1603
1009
  }
1604
1010
  if (!ctx)
1605
1011
  return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
1606
- // Rebuild taskConfig via resolveSpec with auth reconstructed from the checkpoint (sessionId + scope).
1607
1012
  const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
1608
- // E18: STRIP resumeAt before re-resolving on a durable resume — the leaf is already set by the checkpoint, and
1609
- // resumeAt + a durable resume is rejected by core (resume_at.conflicts_resume); also avoids re-resolving a stale
1610
- // anchor (which could 404 if the session was purged). A resume-at run that suspended stored its body with resumeAt.
1611
- // 对抗评审 2026-07-11(MED):resolveSpec re-runs the ADMISSION gates (scenario/execution ruling — resume does
1612
- // NOT freeze governance, the established precedent posture; a deny is ALSO a model run, so it is gated too). But an
1613
- // escaping HttpError here was (a) attributed to the OPERATOR's /decide request as a bare 400/403 — undiagnosable
1614
- // from that contract — and (b) swallowed by the D-D SLA deny-sweep's catch, silently failing EVERY tick. Fold it
1615
- // into a typed 409 result instead: the operator sees "this parked task is blocked by a policy change" (retry
1616
- // after the policy is restored, or let terminal_at_ms abort it); the sweep skips the row this tick (retried next
1617
- // tick, terminal_at_ms backstop — its documented per-call-failure semantics).
1618
1013
  let spec;
1619
1014
  try {
1620
1015
  spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
@@ -1628,41 +1023,14 @@ export function createHttpServer(rawDeps) {
1628
1023
  }
1629
1024
  throw e;
1630
1025
  }
1631
- // Typed pre-check: the pending tool must still exist in the rebuilt config; a scenario
1632
- // redeploy that removed it means the approval can no longer be applied — fail clearly, never a 500/silent.
1633
- // The "hand" tools are NOT in spec.tools — core mounts them at the runner from the executionEnvFactory
1634
- // (`HAND_TOOL_NAMES` above = hands-lane.ts `HANDS_BAND_TOOL_NAMES`, the single owner; nothing is mirrored here).
1635
- // When this deployment has remote exec configured the rebuilt task WILL expose them, so union them in; else
1636
- // gating a hand tool (the common code-agent case) would always 422 on resume.
1637
- // #196 旁注:本 union 只看部署有没有 remoteExec,不看本任务的 hands lane —— 对 hands=none 的任务这是
1638
- // **过近似**(那些工具根本没 mount,也就不可能有它们的 park 行),只影响 422 的 UX 宽严,不放大任何权限。
1639
- // #244:union 必须是 band **全**名单——effects 表外的挂载补集(Monitor/EnterWorktree/…)当年漏了,
1640
- // 断连 force-park 人群第一个踩中(approve 恒 422,重呈卡回答永不传导;test legB1-durable 实测指纹)。
1641
- // [2380] RB-476(5.0.0):Q6 折叠预检退役——满足性匹配按 RAW 名。pre-floor 旧名 checkpoint
1642
- // (`bash`/`run_workflow` 等)在此 422 诚实拒,与 core 自己的 resume.tool_unavailable 同方向
1643
- // ([2364] 令:耐久面响亮牺牲 + P-7 重开,不静默兜)。现役名恒自映射。
1644
1026
  const pendingTool = cp.pendingAction.toolName;
1645
1027
  const pendingToolCanon = pendingTool;
1646
1028
  const availableTools = new Set((spec.tools ?? []).map((t) => t.name));
1647
1029
  if (deps.config.remoteExec)
1648
1030
  for (const name of HAND_TOOL_NAMES)
1649
1031
  availableTools.add(name);
1650
- // AskUserQuestion (durable ask, TC-5.4) is ALSO not in spec.tools — core mounts it from `onQuestion`
1651
- // (#152: the QUESTION_AWAITS_RESUME sentinel when no live coordinator is wired, else RunnerDeps.onQuestion's
1652
- // coordinator serves the seat). Without this it 422s on resume exactly like a hand tool would. Gated on
1653
- // checkpointStore = durable mode is active.
1654
1032
  if (deps.checkpointStore)
1655
1033
  availableTools.add("AskUserQuestion");
1656
- // Workflow (codex round-2 on [1245]/[1248]②): `run_workflow` is ALSO mounted at the runner, never in
1657
- // spec.tools — core mounts it when `spec.selfOrchestration === true` and the deployment wires the workflow
1658
- // deps (isSelfOrchestrationActive, dist-read). Without this, approving a durably-parked workflow ask
1659
- // (the new mode-derived leg) 422s here and the suspend can only ever be denied. Same over-approximation
1660
- // posture as the hand-tools union above (a per-principal allowWorkflows=false revoked between suspend and
1661
- // approve slips this pre-check; core's own resume path then fails loudly — the pre-check is a UX guard,
1662
- // not the authority).
1663
- // A-033.1(#256):名字从 core 的 `RUN_WORKFLOW_TOOL_NAME` 取,不再手抄 —— 手抄的名字在 core 改名后
1664
- // 与实挂名分家,这一格会把**每一次**已批准的 workflow 赎回打成 422(pendingTool 是实挂名,集合里
1665
- // 却是旧名),而 task-settings 那三处同族手抄的后果更重(ask/deny 门整条静默不触发)。
1666
1034
  if (spec.selfOrchestration === true && deps.config.selfOrchestrationEnabled) {
1667
1035
  availableTools.add(RUN_WORKFLOW_TOOL_NAME);
1668
1036
  }
@@ -1672,25 +1040,6 @@ export function createHttpServer(rawDeps) {
1672
1040
  body: { error: `pending action no longer satisfiable: tool "${pendingToolCanon}" is not in the current task config (scenario changed since suspend)`, errorCode: "conflict.pending_action_unsatisfiable" },
1673
1041
  };
1674
1042
  }
1675
- // Durable-ask request/state coherence (TC-5.4), all pre-CAS so a mismatched request never consumes the
1676
- // checkpoint:
1677
- // - approving a pending AskUserQuestion REQUIRES body.answer. Without it the redeemed call executes against
1678
- // whatever question face the rebuilt spec has, and the operator's question is unrecoverably LOST (checkpoint
1679
- // consumed, no answer ever delivered). Fail the request instead; the operator retries with the answer.
1680
- // 🔴 #152: this check is now the ONLY one on this route. core has its own pre-CAS refusal for
1681
- // "approve a content-ask with no answer" (runtask.js: `taskConfig.onQuestion ?? deps.onQuestion` is
1682
- // `undefined` or QUESTION_AWAITS_RESUME), but on an ASK_QUESTION_ENABLED deployment the spec no longer
1683
- // stamps the sentinel and `deps.onQuestion` is the coordinator, so core's condition is never met and it
1684
- // does NOT refuse.
1685
- // ⚠️ The ORIGINAL rationale — "the redeemed leg would take the coordinator's empty 'no human' default" —
1686
- // is DISPROVED and must not be repeated: since #166 QuestionCoordinator never synthesizes an empty answer
1687
- // set (every no-human arm returns `{kind:"unavailable"}`), and core 5.16 answers a redeemed AskUserQuestion
1688
- // with an isError ("will not silently self-answer") rather than a fabricated verdict. The guard STAYS on a
1689
- // cheaper rationale: the checkpoint is consumed by the CAS BEFORE that revive can report unavailable, so
1690
- // the operator loses the parked question for a revive that could only fail — refusing pre-CAS costs nothing
1691
- // and gives a precise error. Do not delete it (parked twin: src/parked-decide.ts, same rationale).
1692
- // - an answer is only meaningful on an approve of a question gate; on a deny or a non-question gate it
1693
- // signals the operator is deciding a different pending action than the one parked — reject, don't guess.
1694
1043
  if (decision === "approve" && pendingTool === "AskUserQuestion" && !answer) {
1695
1044
  return { status: 400, body: { error: "pending action is AskUserQuestion — approve must carry body.answer ({ answers: [{ header, selected: string[], note? }] })", errorCode: "request.field_conflict" } };
1696
1045
  }
@@ -1698,17 +1047,6 @@ export function createHttpServer(rawDeps) {
1698
1047
  return { status: 400, body: { error: `body.answer is only valid when approving a pending AskUserQuestion (decision: "${decision}", pending: "${pendingTool ?? "unknown"}")`, errorCode: "request.field_conflict" } };
1699
1048
  }
1700
1049
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
1701
- // Durable ask (TC-5.4 → RB-459, core 5.7.0): the operator's answer rides the ResumeOutcome — core's
1702
- // answerFaceForRedeemedCall replays it to the REDEEMED question call only, so a NEW question on the
1703
- // resumed leg gets an honest error instead of a silently replayed stale answer (which the pre-5.7.0
1704
- // `onQuestion = async () => answer` closure form did). Absent answer ⇒ outcome unchanged (an F4 tool
1705
- // approval resumes exactly as before). The onQuestion seat is left alone.
1706
- // D-1 decision-action binding (core 1.101, design/80 §2): echo the boundCallId + boundInputHash the operator
1707
- // saw back into the outcome — core verifies opaque STRING equality against the persisted suspend-mint values
1708
- // (fail-closed, PRE-CAS), so "saw action A, resolve B" is rejected. boundInputHash is NEVER recomputed here
1709
- // (the engine minted it once via canonical-json; a second runtime re-serializing would false-mismatch and
1710
- // fail-closed reject a legit approval). Legacy callers that omit the fields fall back to the current persisted
1711
- // values (no human binding — pre-D-1 behavior). resource_limit pendings carry no action → empty (core gate_mismatch).
1712
1050
  const pa = cp.pendingAction;
1713
1051
  const persistedCallId = pa.kind === "tool_approval" ? pa.toolCallId : "";
1714
1052
  const persistedHash = pa.kind === "tool_approval" ? (pa.boundInputHash ?? "") : "";
@@ -1720,74 +1058,24 @@ export function createHttpServer(rawDeps) {
1720
1058
  ...(decision === "approve" && binding?.updatedInput !== undefined ? { updatedInput: binding.updatedInput } : {}),
1721
1059
  ...(reason ? { reason } : {}),
1722
1060
  ...(answer !== undefined ? { answer } : {}),
1723
- // #204 件6:结算出处逐字上 outcome(见参数顶注)。core 只接受它自己那三个词,server 不派生、不缺省。
1724
1061
  settledBy,
1725
1062
  };
1726
- // Developer-mode verify (1.44) survives suspend/resume (core `resumeWithVerification`, design/51 P1-b): a
1727
- // task submitted with verify:true that hit an F4 gate must STILL be gated by the adversarial verifier on
1728
- // completion — a plain resumeStream would silently drop the gate. The flag rides in the persisted ctx.body
1729
- // (same source the spec was rebuilt from). Like first-run verify it's multi-attempt (not a live stream), so
1730
- // we await the result + write a single terminal event rather than streaming post-resume deltas.
1731
1063
  const verifyRounds = verifyRoundsFromBody(ctx.body);
1732
- // taskId up-front: the resumed leg streams its post-resume events into the SAME durable run log, CONTINUING
1733
- // the seq past the suspend (core 1.70 `resumeStream`) — so GET /v1/runs/:id/events covers post-resume too,
1734
- // observability parity with runInBackground (closed by core 1.70).
1735
- // ── Drive the resume into the durable run log via the SHARED machinery (driveResumeIntoRunLog): markResuming
1736
- // CAS, the cancel + preempt controllers (same-replica fast path + cross-replica flag polls), the resume
1737
- // heartbeat, the resumeStream / resumeWithVerification loop, CheckpointError classification, re-suspend vs
1738
- // terminal drive. This HITL /decide path supplies a `policy_ask` outcome; the scheduler preempt path supplies
1739
- // a `resource_limit`/continue one — sharing this prevents the two resume entries from drifting.
1740
1064
  return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome, verifyRounds, ...(onResumeCommitted ? { onResumeCommitted } : {}), ...(acceptEarly === true ? { acceptEarly: true } : {}) });
1741
1065
  }
1742
- /**
1743
- * The resume-drive machinery SHARED by `resumeCheckpoint` (operator /decide, `policy_ask`) and `resumePreempted`
1744
- * (scheduler resume, `resource_limit`/continue). Given the already-located `token`, the rebuilt
1745
- * `taskConfig`/`resumeObjective`, the gate-specific `outcome`, and optional `verifyRounds`, it: markResuming-
1746
- * CASes the parked row to `running` (idempotent across replicas), registers the leg in BOTH `inflightRuns`
1747
- * (cancel) and `preemptableRuns` (re-preempt) for the same-replica fast path, runs a heartbeat that polls the
1748
- * durable cancel + preempt flags (cross-replica), resumes via resumeStream / resumeWithVerification threading a
1749
- * FRESH cancel signal AND a FRESH preemptSignal (so a resumed leg is itself cancel-able + RE-preempt-able —
1750
- * never the consumed suspend-time signal), classifies a CheckpointError, handles a re-suspend
1751
- * (setSuspended, keep the lock) vs a terminal (setTerminal), and returns the HTTP {status, body}.
1752
- *
1753
- * #316:**200 受理形的分派器**。`args.acceptEarly !== true` ⇒ 逐字旧行为(整条腿 await 完再回)。
1754
- * 传 `true` ⇒ 在受理点(`driveResumeLeg` 里 `resumeStream` 解析之后)先把受理体交回,模型腿继续在
1755
- * 同一条 promise 链上跑完。判据(受理点为什么在那里、哪三类判定必须留在同步侧)见
1756
- * {@link DriveResumeArgs.acceptEarly} 的顶注 —— 那是这条语义的唯一属主,别在这里第二次成文。
1757
- *
1758
- * 🔴 **受理之后的失败绝不静默**:HTTP 已经走了,行/账本是权威可见面(腿内部各分支照旧驱行终局),
1759
- * 但那条链的**结局**在这里必须留痕 —— 否则「受理了然后呢」在遥测里没有任何线索。
1760
- *
1761
- * 🔴 **如实登记的一条能见度差额**(codex 对抗复审 R1-[high],验真后按真形收窄):受理之后**行驱动写**
1762
- * (`setTerminal`/`setSuspended`,腿尾那段)如果自己炸了,调用方拿不到任何东西 —— 修前那一形是 500。
1763
- * ⚠️ 但**持久状态的分歧是既存的、两形逐字相同**:同步形下 500 并不会修好任何东西(账本可能已落
1764
- * `done` 而行仍 `running` 且攥着 session claim,靠 reapStale 收;LOCAL/file 车道无周期腿 ⇒ 上界 ∞,
1765
- * [4664]② 洞 (d))。⇒ 本批的真差额 = **调用方能见度**,不是新增的数据分歧;补偿 = 下面这条 error 级
1766
- * 留痕 + `resume_post_accept_failed_total` 计数(受理前的 4xx **不**计入,见 `accepted` 标志)。
1767
- * 真正的根治(受理前durable 记一条可续的续跑意图 + 行驱动重试对账)是**另一批**的设计件,已上报。
1768
- */
1769
1066
  async function driveResumeIntoRunLog(args) {
1770
1067
  if (args.acceptEarly !== true)
1771
1068
  return await driveResumeLeg(args);
1772
1069
  let settleAccepted;
1773
- // 🔴 codex 对抗复审 R1-[medium](验真后修):遥测必须能分辨「受理前的正常拒绝」与「受理后的脱手
1774
- // 失败」。处理器挂在 race **之前**(它必须无条件挂:否则受理前就 reject 的腿会变成进程级
1775
- // unhandledRejection),所以判别位不能靠「处理器有没有跑」——用受理回调**同步**置起的这个标志。
1776
- // 没有它:每一次 429 admission 拒 / 409 CAS 输 / pre-CAS 绑定不符都会被记成「受理后失败」,把这条
1777
- // 新信号淹掉(常态冲突远多于真脱手失败)。
1778
1070
  let wasAccepted = false;
1779
1071
  const accepted = new Promise((resolve) => { settleAccepted = resolve; });
1780
1072
  const leg = driveResumeLeg(args, (receipt) => { wasAccepted = true; settleAccepted?.(receipt); });
1781
1073
  void leg.then((out) => {
1782
- // 受理前的 4xx = 正常的同步拒绝(它就是调用方拿到的那个响应),不是本信号的事。
1783
1074
  if (wasAccepted && out.status >= 400) {
1784
1075
  deps.logger?.warn?.("resume_leg_failed_after_accept", { sessionId: args.sessionId, status: out.status, body: out.body });
1785
1076
  deps.metrics?.inc("resume_post_accept_failed_total");
1786
1077
  }
1787
1078
  }, (err) => {
1788
- // 腿内部对「普通异常」已经驱行终局(claimedRow 守卫那支),这里补的是**可观察性**那一半。
1789
- // 受理**前**抛的(race 会把它原样传给调用方 ⇒ 500)同样不计入本信号——那条路的可观察性归
1790
- // 路由层的错误面,重复计数只会让「脱手失败」这个读数失去判别力。
1791
1079
  if (!wasAccepted)
1792
1080
  return;
1793
1081
  deps.logger?.error?.("resume_leg_threw_after_accept", { sessionId: args.sessionId, err: err instanceof Error ? err.message : String(err) });
@@ -1795,33 +1083,15 @@ export function createHttpServer(rawDeps) {
1795
1083
  });
1796
1084
  return await Promise.race([accepted, leg]);
1797
1085
  }
1798
- /**
1799
- * {@link driveResumeIntoRunLog} 的**腿本体**(实现逐字未动,只多了一个受理回调)。
1800
- *
1801
- * `onAccepted` = #316 的受理点回调:在 `resumeStream` 解析之后、流迭代之前触发**一次**(有 run 行时
1802
- * 才触发 —— 没有行就没有可 poll 的终态面,那时提前受理等于把结果扔了)。缺席 ⇒ 全同步旧形。
1803
- */
1804
1086
  async function driveResumeLeg(args, onAccepted) {
1805
- // #196:`runner` = 本次续跑的执行 Runner(场景 hands 判别位裁定,与首跑同源;见 DriveResumeArgs 注)。
1806
1087
  const { runner: legRunner, token, sessionId, principal, fleetScope, taskConfig, resumeObjective, outcome, verifyRounds } = args;
1807
- // ── #151 车3 刀 3b:resume 腿的 `legKey` 真值(设计稿 §3.2′)──────────────────────────────────
1808
- // 一次 park→resume 的 checkpoint token **就是**这条腿的天然身份:同一 token 重投 = 同一条腿(幂等,
1809
- // askId 不变);新 park ⇒ 新 token ⇒ 新腿 ⇒ 新 askId ⇒ **重新征询**(§3.3「resume 只重放未完成兄弟」)。
1810
- // 🔴 存**摘要**不存原文:token 是能力凭据(本仓有 `stripCheckpointToken` 专门把它从可重放账本/结果里
1811
- // 剥掉),而 `askId` 是 wire 可见值 —— 摘要单向,原始 token 不进任何行、不进任何帧。
1812
- // 入口算一次(这里就是唯一持有 `args.token` 的地方),下面随 ctx 走。
1813
1088
  const legKey = createHash("sha256").update(String(token)).digest("hex");
1814
- // §7.3:本 leg 的 walltime deadline —— 取值点 = 本腿开始执行之前(`resumeStream` 调用在下方)。
1815
- // 与 sync/bg 两腿同一条判据:只在协议上场时供值,误差方向偏短(安全侧),理由见 routes/tasks.ts 同处注。
1816
1089
  const streamApprovalOn = resolveStreamApprovalGate({
1817
1090
  toolApprovalEnabled: Boolean(deps.toolApproval),
1818
1091
  streamApprovalEnabled: deps.config.streamApproval?.enabled === true,
1819
1092
  backend: deps.backend,
1820
1093
  parkFacility: deps.checkpointStore !== undefined,
1821
1094
  }).active;
1822
- // 🔴 codex 交叉复审 F4(2026-08-06 真 finding)的修:本腿此前**无条件**包 approval ALS(只把新协议的
1823
- // 口与轴挂在 `streamApprovalOn` 后面)⇒ ①协议关时本腿凭空多了一条活卡投递路(此前恒 park,A-1 破)、
1824
- // ②窗=0 在本腿上不生效。裁定走**三腿共用**的 `resolveApprovalLeg`,一份判据三处消费。
1825
1095
  const approvalLeg = resolveApprovalLeg({
1826
1096
  streamApprovalOn,
1827
1097
  windowMs: deps.config.streamApproval?.windowMs ?? 0,
@@ -1830,11 +1100,6 @@ export function createHttpServer(rawDeps) {
1830
1100
  nowMonotonicMs: performance.now(),
1831
1101
  });
1832
1102
  const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
1833
- // 🔴 复审 C2:lease admission for EVERY resume family (/decide, /answer, /plan_review, preempt-resume)
1834
- // funnels through here — keyed on `principal` = the CHECKPOINT OWNER, the SAME identity the resumed model spend
1835
- // is billed to via `withPrincipal(principal)` below. The route guards deliberately do NOT lease-gate (they'd
1836
- // key on the request principal = a possibly-cross-tenant operator). A live lease admits locally; center-judged
1837
- // exhaustion of the OWNER's budget returns 429 (mirroring leaseDenied's shape); an unreachable center fails open.
1838
1103
  if (deps.fleetLease && principal) {
1839
1104
  const adm = await deps.fleetLease.admit(principal);
1840
1105
  if (!adm.ok) {
@@ -1843,20 +1108,7 @@ export function createHttpServer(rawDeps) {
1843
1108
  }
1844
1109
  }
1845
1110
  let taskId = await deps.runStore?.getActiveTaskId(sessionId);
1846
- // [3829]/[3830] wake 腿(mintFreshRun 声明位):task_done 纯 park 的终局 finalize 已删 task_active
1847
- // claim——本腿在此恒拿不到 taskId,曾经整腿无行裸跑(不可取消/无计费归因/无 durable append/fleet
1848
- // 不可见),而 core `spec.taskId ?? sessionId` fallback 还会把 lastRunId 铸成 sessionId(续听恒 404,
1849
- // cli 7.18 提货围栏首撞)。wake 续跑=同 session 新一轮 turn(原任务已终局),正确形=铸新 canonical
1850
- // taskId + createRun 全生命周期行;createRun 的 task_active 唯一键 claim 同时补上 double-text 互斥
1851
- // (getActiveTaskId 读后另一提交抢建行的竞态窗 ⇒ 与 sync 腿同形 409 真出路)。未声明该位的腿
1852
- // (decide/answer/plan_review)语义不变:claim 缺席=行被 reap 的竞态,checkpoint token 同拍过期兜底。
1853
1111
  let freshRunClaimed = false;
1854
- // 🔴 codex R1-F1(2026-08-14):mintFreshRun 腿对「claim 有值」**只有 409 一条出路**,绝不落入下方的
1855
- // markResuming 复用臂。本腿的 park 是终局后纯 park——它自己的行早已终局并释放 claim,此刻 claim 里
1856
- // 的必是**别的 run**(典型:task_done park 之后同 session 又提交了新任务且其已 park 成 suspended,
1857
- // claim 保留)。旧路径会用**本 park 的旧 token**对**对方的行**赢下 markResuming CAS 并驱动续跑——
1858
- // 错误终局化对方的行、孤儿化对方仍 pending 的卡(checkpoint 表无 per-session pending 唯一约束,
1859
- // findPendingTokenBySession 又是无序 LIMIT 1,两条 pending 并存时旧 task_done park 完全可能被选中)。
1860
1112
  if (args.mintFreshRun && taskId && deps.runStore) {
1861
1113
  return { status: 409, body: await buildActiveRunConflict({ runStore: deps.runStore, checkpointStore: deps.checkpointStore, governance: deps.config, runStaleSec: deps.config.runStaleSec, turnActivity: readTurnActivityMs }, sessionId, taskId) };
1862
1114
  }
@@ -1872,54 +1124,23 @@ export function createHttpServer(rawDeps) {
1872
1124
  taskId = freshId;
1873
1125
  freshRunClaimed = true;
1874
1126
  }
1875
- // MF-Fleet (TOC review #7): a HITL resume (/decide, /answer, /plan_review, /preempt-resume, /wake) drives the
1876
- // SAME durable run row that suspended — so it must re-publish to the fleet bus under the SAME runId, or the run
1877
- // VANISHES from GET /v1/fleet/stream the moment it resumes (the original publisher's row was last touched at
1878
- // suspend; nothing keeps it live or settles it after). Reuse `taskId` (the existing suspended run id) so the row
1879
- // RE-APPEARS as the same fleet row; scope = `principal` (the checkpoint/run-owner scope persisted at suspend —
1880
- // the original submitter, NOT spoofable; matches the run row's owner). rootTaskId = taskId(codex F3,2026-08-13):
1881
- // resume 的 taskConfig 自此携带 spec.taskId=run 行 id(下方注入点),core canonical 随之——[3806] 修完 sync
1882
- // 两腿后本腿曾是最后一条 sessionId fallback 腿(resume 后 noteTaskRun 再铸 lastRunId 污染)。No-op when no
1883
- // fleetBus / no taskId.
1884
- // 🔒 scope = `fleetScope` (the VERIFIED `gatedPrincipal(req)` the calling route computed) — the SAME identity
1885
- // GET /v1/fleet/stream filters by, mirroring the create-path #6 fix. NOT `principal` (= the spoofable
1886
- // checkpoint/auth header from `principalFrom`): a spoofed header would re-publish the resumed row under a
1887
- // victim's scope (cross-tenant leak) or under "default" while the real owner's stream filters it OUT (the row
1888
- // vanishes on resume). Tenant-isolation parity with the stream filter on the direct resume door (TOC review #6).
1889
- // onForwardEvent parity: core 1.210 added the RunInternals 4th arg to `resumeStream` — the
1890
- // resume leg now feeds the forward sink like the sync/bg legs (fleet child rows at any depth + the durable
1891
- // task_progress append; see the resumeStream call in driveResume).
1892
1127
  const fleetPub = taskId && deps.fleetBus
1893
1128
  ? fleetRunPublisher(deps.fleetBus, { runId: taskId, scope: fleetScope, rootTaskId: taskId, ...fleetRunLabels(resumeObjective) })
1894
1129
  : undefined;
1895
- // codex F3(2026-08-13):resume 重建的 taskConfig 必须携带 run 行 tid——缺席时 core `spec.taskId ??
1896
- // sessionId` fallback 喂 noteTaskRun,resume 一次 lastRunId 就再被污染成 sessionId([3806] sync 两腿
1897
- // 修复的 resume 腿孪生)。canonical taskId 迁移的三个同源消费点(fleet rootTaskId / stoppedBy owner /
1898
- // model-usage 归因键)本批同改,勿单点回退。
1899
1130
  const resumeTaskConfig = taskId ? { ...taskConfig, taskId } : taskConfig;
1900
- // C (TOC adversarial review): settle the resumed fleet row EXACTLY once. A plain Error thrown out of the
1901
- // resume (not CheckpointError/cancel) hits the catch's `throw e` BEFORE the final onTerminal below → the
1902
- // re-published row leaks as a stale "running". Route every onTerminal through this guard so the finally can
1903
- // settle a row no branch reached, without double-settling one that did.
1904
1131
  let fleetSettled = false;
1905
1132
  const settleFleet = (status, residuals) => {
1906
1133
  if (fleetSettled)
1907
1134
  return;
1908
1135
  fleetSettled = true;
1909
- fleetPub?.onTerminal(status, residuals); // [1839]①/(a):真终态帧带残局键(publisher 侧对 parked 再挡一层)
1136
+ fleetPub?.onTerminal(status, residuals);
1910
1137
  };
1911
1138
  let result;
1912
- // [1245] codex-5: the resume.* code ALONE does not prove the reopen landed — installed core (1.330) only
1913
- // rewrites to checkpoint.reopen_failed when store.reopen returned FALSE; a THROWING reopen (plain DB error)
1914
- // keeps the resume.* code with the checkpoint still resolved. Confirm against the store before re-parking:
1915
- // this token pending again ⇒ real re-park; not pending ⇒ fall through to the terminal/done leg; probe error
1916
- // ⇒ re-park anyway (the safer side under uncertainty: a wrongly-suspended row is reaper-bounded
1917
- // (reapSuspended), a wrongly-terminal row orphans a live park until TTL — the round-3 bug shape).
1918
1139
  let reopenConfirmedPark = false;
1919
1140
  const confirmCheckpointReopened = async () => {
1920
1141
  const store = deps.checkpointStore;
1921
1142
  if (!store)
1922
- return true; // durable resume required the store; absence here = cannot confirm → the safer side
1143
+ return true;
1923
1144
  try {
1924
1145
  return (await store.findPendingTokenBySession(sessionId)) === token;
1925
1146
  }
@@ -1927,59 +1148,22 @@ export function createHttpServer(rawDeps) {
1927
1148
  return true;
1928
1149
  }
1929
1150
  };
1930
- // Protect the in-flight resume from the reaper: flip the parked row to `running` + heartbeat for the
1931
- // duration of the resume. The run row otherwise stays `suspended`/updated_at=suspend-time the whole time
1932
- // (resume neither rewrites the row nor heartbeats), so `reapSuspended` would mark a live resume `failed`
1933
- // and release its session lock once it crossed the approval TTL. Cleared in the finally below.
1934
1151
  let resumeHeartbeat;
1935
- let resumeManifestLease = 0; // [998]②: this leg's tracker lease (assigned at register, checked by the finally's clear)
1936
- // 1.78 (review LOW, pre-existing): THIS leg's live TaskStream — held so the finally can identity-guard its
1937
- // steerableRuns deregistration (an unconditional delete could clobber a handle a FASTER sibling leg just
1938
- // registered under the same taskId across a suspend→fast-resume).
1152
+ let resumeManifestLease = 0;
1939
1153
  let resumeStreamRef;
1940
- let resumeLegLive = false; // true only while the resumed leg is live (set at resumeStream, cleared in the finally)
1941
- // design/122 ③: this leg's subagent-handle unregister thunks — run at leg end (finally), not on settle.
1154
+ let resumeLegLive = false;
1942
1155
  const subagentHandleEvictions = [];
1943
1156
  let claimedRow = false;
1944
- // Cancel parity with runInBackground: the resumed leg is registered in `inflightRuns` (same-replica fast
1945
- // path) and its heartbeat polls the durable cancel flag (cross-replica) — without this a resumed run was
1946
- // un-cancellable (cancel 202'd + flagged the row, but nothing honored it). markResuming reset the flag,
1947
- // so only a cancel issued DURING this leg aborts it.
1948
1157
  const cancelCtrl = new AbortController();
1949
- // 🔴 stoppedBy 归因的注册点在**下面**(markResuming 认领成功之后),不在这里 —— #168 件3,见那里的注。
1950
- // design/80 seam #2: a resumed leg is itself preempt-able. Register a FRESH preempt controller (markResuming
1951
- // also reset preempt_requested), thread a LIVE preemptSignal into the resume taskConfig below, and poll the
1952
- // durable preempt flag in the heartbeat — so the scheduler can preempt the RESUMED work once it makes
1953
- // progress. 🔴 Never re-supply the consumed suspend-time signal (core strips a pre-aborted one).
1954
1158
  const preemptCtrl = new AbortController();
1955
1159
  if (taskId && deps.runStore) {
1956
- // The CAS fails (false) only if the row is no longer `suspended` — the reaper already expired this
1957
- // approval (and released the session lock), the run is terminal, or a concurrent resume already claimed
1958
- // it. Bail rather than run a resume whose run row is gone / a sibling owns; the checkpoint reaper expires
1959
- // the token in the same tick so resumeStream would reject anyway. Closes the window the heartbeat can't.
1960
- // [3829]:fresh 行由上面的 createRun 事务铸出即 `running` + claim 在手——markResuming 的 CAS 只认
1961
- // suspended/needs_review,对它恒败;跳过=行本来就是本腿自己的,资格等价于 CAS 赢。
1962
1160
  claimedRow = freshRunClaimed ? true : await deps.runStore.markResuming(taskId);
1963
1161
  if (!claimedRow)
1964
1162
  return { status: 409, body: { error: "run is not in a resumable (suspended) state (already resumed, decided, or expired)", errorCode: "conflict.not_resumable" } };
1965
- // stoppedBy — resume leg NOW marked (core 1.256 delivered the mark-by-owner seam): codex F3(2026-08-13)
1966
- // 起本腿 canonical taskId == run 行 taskId(resumeTaskConfig 注入 spec.taskId),task-scoped 子代的 owner
1967
- // 键值随之与 session-scoped 的分开——但 seam 仍按 EXPLICIT flag 过滤(`skipSessionScoped:true` inside
1968
- // the helper),only task-scoped children take the "user" marker; session-scoped ones a cancelled parent
1969
- // deliberately does NOT stop stay unmarked (CC Backgrounded semantics). Owner = taskId(this leg's
1970
- // canonical key since the F3 injection), NOT sessionId.
1971
- //
1972
- // 🔴 位置(#168 件3,与 /v1/tasks/stream 同族):归因规则只有**抢到这条 run 的腿**才有资格挂 ——
1973
- // `markResuming` 输掉的那一支(别的副本已经在 resume,或行已被 reap)在上面就 409 走人,从此一次
1974
- // 都不注册。本腿此前把它挂在 CAS 之前,只是因为 `cancelCtrl` 的唯一 abort 源(inflightRuns / 心跳
1975
- // 轮询)也都在 CAS 之后注册才没出事 —— 那是巧合不是保证。
1976
- markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal); // owner=canonical taskId(codex F3:resumeTaskConfig 带 spec.taskId=run 行 id,core 子代 owner 键随之;此臂在 if(taskId) 块内恒真)
1977
- // MF-Fleet (#7): the row is now `running` again → re-publish it LIVE to the fleet (re-appears as the same row
1978
- // that was sitting "waiting" while parked). After onStart only — never if markResuming lost the CAS (a sibling
1979
- // owns the row). onTerminal at every settle below keeps the row's lifecycle in lock-step with the run-store row.
1163
+ markChildrenStoppedByUserOnAbort(cancelCtrl.signal, taskId, principal);
1980
1164
  fleetPub?.onStart();
1981
1165
  const rs = deps.runStore;
1982
- const owner = principal ?? null; // the resumed run's owner (the original submitter) — single-DB null-safe guard
1166
+ const owner = principal ?? null;
1983
1167
  inflightRuns.set(taskId, cancelCtrl);
1984
1168
  preemptableRuns.set(taskId, preemptCtrl);
1985
1169
  resumeHeartbeat = setInterval(() => {
@@ -1994,98 +1178,39 @@ export function createHttpServer(rawDeps) {
1994
1178
  }
1995
1179
  }, HEARTBEAT_MS);
1996
1180
  resumeHeartbeat.unref?.();
1997
- deps.modelUsage?.register(taskId); // E8 leak fence: track the resumed leg's brain.calls (sub-tasks excluded); cleared in the finally
1998
- resumeManifestLease = deps.promptManifests?.register(taskId) ?? 0; // [998]②: the resumed prepare re-fires prompt.assembled — this leg drains its own manifest; lease guards the finally's clear (B-3 window)
1181
+ deps.modelUsage?.register(taskId);
1182
+ resumeManifestLease = deps.promptManifests?.register(taskId) ?? 0;
1999
1183
  }
2000
- // codex M2 hook point: the CAS (when a run row exists) is decided above — a lost race already returned 409 and
2001
- // never reaches here. Awaited BEFORE the model drive so the resumed leg's first ask observes the side-effects.
2002
1184
  if (args.onResumeCommitted)
2003
1185
  await args.onResumeCommitted();
2004
- // #310 `engine_notice` wire 腿(三腿之一:**resume**)。声明在 try 之外、赋值在 `append` 铸出之后
2005
- // (账本写口在 withPrincipal 闭包里才成形),注销在下方同一个 finally —— 任何退出路径都过它。
2006
1186
  let unregisterEngineNotice;
2007
1187
  try {
2008
- // Wrap in withPrincipal so the resumed leg's LLM spend is attributed to the tenant's CostQuota — parity
2009
- // with the sync/async paths (council: otherwise maxPrincipalCostUsd is silently bypassed on resume).
2010
1188
  result = await withPrincipal(principal, async () => {
2011
- // P1 ①② follow-on: drain the async-workflow completion inbox on the RESUME leg — hoisted
2012
- // ABOVE the verify/stream fork so BOTH resume flavors get it (parity with runs.ts, whose drain runs
2013
- // "before ANY branch"; review round 2 caught the verify-leg gap). A workflow that finished while this run
2014
- // sat suspended surfaces as a durable `workflow_complete` event the tailing client replays (at-least-once
2015
- // + shell runId-dedup). Owner gate = the original submitter (the checkpoint scope) — the identity the
2016
- // entries were stamped with at enqueue. 🔒 ONLY when a durable log exists (`deps.runStore && taskId`):
2017
- // without one there is nowhere to persist the frame, and `drain` REMOVES entries — draining there would
2018
- // EAT the completions. One-shot appender off maxSeq; each branch below re-reads maxSeq, so seq stays safe.
2019
- // option A: keys this leg already delivered (turn-open drain, loop case OR observer) — hoisted ABOVE the
2020
- // drain so the drained frames' keys register here too (core [784]④b: the same idle-arrival is also parked
2021
- // core-side; its pend-drain replay rides the resumed stream as a task_notification event — the loop's key
2022
- // check below folds that second durable append). codex F1: NotifiedKeys — the observer arm appends
2023
- // fire-and-forget, so claims carry the write's outcome promise (bg-leg twin's rationale).
2024
1189
  const resumeNotifiedKeys = new NotifiedKeys();
2025
1190
  if (deps.runStore && taskId) {
2026
1191
  const rs0 = deps.runStore;
2027
1192
  let drainSeq = (await rs0.maxSeq(taskId)) + 1;
2028
1193
  await emitPendingWorkflowCompletions(deps.workflowCompletionInbox, sessionId, principal ?? null, (frame) => {
2029
- // 🔴 本仓唯一的**动态**账本写口:`type` 逐字来自 core 帧,不是本仓字面量,所以闭集
2030
- // `LedgerEventType` 在这里只能是**声明**而非证明(A-032 P1-③ 射程上限,ledger-events.ts 顶注
2031
- // 有同款自陈)。今天这条腿产出的是 `task_notification`;core 换帧名时**本处不会红**,对齐由
2032
- // core 帧集的消费门(`trace/core-keyset-guard.ts` 一族)负责。
2033
1194
  const { type, ...rest } = frame;
2034
- // codex F1 + 修2(三路复审 notify-1):the claim must carry the WRITE OUTCOME, not a pre-resolved
2035
- // true — a transient append failure with a `Promise.resolve(true)` claim would fake-suppress the
2036
- // stream/core-side replay of the SAME key while the durable row never landed (the un-acked inbox
2037
- // entry re-delivers next leg, but this leg's replay was eaten). Mirror of the bg-leg/runs.ts twin.
2038
- // codex F2: task_type rides the key (external 帧独立键域 — bg twin's note).
2039
1195
  const f = frame;
2040
1196
  const write = rs0.appendEvent(taskId, drainSeq++, type, rest);
2041
1197
  if (f.type === "task_notification" && f.task_id)
2042
1198
  resumeNotifiedKeys.set(taskNotificationStreamKey({ task_type: f.task_type, task_id: f.task_id, status: String(f.status), seq: f.seq }), write.then(() => true, () => false));
2043
1199
  return write;
2044
- },
2045
- // emit-target diagnostics — this drain appends to the resumed run's durable event log
2046
- // (a tailing GET /v1/runs/:id/events client replays it; nothing is written to a live socket here).
2047
- { route: "resume-open", connection: "durable-append", log: (m, x) => deps.logger?.info?.(m, x) });
1200
+ }, { route: "resume-open", connection: "durable-append", log: (m, x) => deps.logger?.info?.(m, x) });
2048
1201
  }
2049
- // Verify task → resume AND re-gate (core resumeWithVerification): resume the impl, and if it COMPLETES
2050
- // run the identical adversarial verifier + fix loop; if it re-suspends it comes back failed-with-token
2051
- // (handled like any re-suspend below). 🔴 Freshness (BUG5, design/53 §2.B): we pass NO `evidence` diff,
2052
- // so the verifier judges the POST-resume working tree directly — there is no stale pre-suspend diff to
2053
- // grade, which closes the "approve then plant a backdoor" window by construction.
2054
1202
  if (verifyRounds !== undefined) {
2055
- // ── #253(core 5.33.0 #246 提货):**这条腿的供给座席**。5.32 及以前 `resumeWithVerification`
2056
- // 的签名里没有 internals 那一位 ⇒ 带 verify 的 resume 对子代进度 / 活性 / bg 终态通知全盲
2057
- // (旁边 resumeStream 臂自 core 1.210 起就有);5.33.0 开了尾可选 `internals`,逐字转发给
2058
- // **恢复实现腿 + 验证器腿 + 每一轮修复腿**。这里把 resumeStream 臂**同一套**供给链接上去。
2059
- // ⚠️ `isolation` 一位永不传(core 在这道门上双相位响亮拒 `config.verify_internals_invalid`)。
2060
1203
  const rsV = deps.runStore;
2061
- // 🔴 **本臂的账本写口 = 共享 `LedgerSink`**(codex 对抗复审 R1 [high],验真后修)。为什么是它
2062
- // 而不是在这里手写三行 append:
2063
- // ① **提交序 == 序号序**:sink 的单写串行链在**链步内**分配序号(它的 [1.211 codex H2] 注)。
2064
- // 此前这条臂的 forward 写是 fire-and-forget、终局/manifest 是 awaited,各自独立发起 ⇒
2065
- // 池化 SQL 后端上**高序号可以先提交**,游标读方(`afterSeq`)推进过去之后,迟到的低序号
2066
- // 行**永久读不到**;
2067
- // ② 序号**一次铸定**:此前在腿跑完之后才 `maxSeq()+1` 现读,与运行期的 forward 写撞号 ——
2068
- // 改成腿开跑前读一次当**基址**,sink 的 1..N 加上去(零 forward 帧时与旧写法逐字等价);
2069
- // ③ forward 帧的**投影**(task_progress / tool_start / tool_end 三型 + 白名单 builder + §E1
2070
- // 脱敏)也归它,本臂不再抄一份 —— 三腿同源正是那个模块存在的理由。
2071
- // ⚠️ **不设「终局即封口」的准入闸**(codex R1/R2 都提了「终局后不该再落行」,**验真后驳回**):
2072
- // 本仓已有属主裁定 —— `runs-coverage` 的 [1840]§三定性钉(mock-anthropic 确定性复现器取证):
2073
- // `run_in_background` **收养形**子代的整个生命周期在宿主腿终态**之后**,live 流按设计 terminal
2074
- // break,帧**照落账**、消费端 done 后重拉即全见;「leg teardown 撕掉 sink」对那一族子代就是
2075
- // **真丢帧**(施工期实测:在 bg 腿加同款围栏,那条钉当场红)。⇒ 本臂与 bg 腿同一条语义:
2076
- // 晚到照落、序号单调。
2077
1204
  const vSeqBase = rsV && taskId ? await rsV.maxSeq(taskId) : 0;
2078
1205
  const verifySink = createLedgerSink({
2079
1206
  appendEvent: (seq, type, data) => (rsV && taskId ? rsV.appendEvent(taskId, vSeqBase + seq, type, data) : Promise.resolve()),
2080
1207
  persistThinking: deps.config.traceThinking,
2081
- notifiedKeys: resumeNotifiedKeys, // 与本腿其余臂共享「已投递的通知键」(bg 腿同款)
1208
+ notifiedKeys: resumeNotifiedKeys,
2082
1209
  ...(taskId ? { onActivity: () => recordTurnActivity(taskId) } : {}),
2083
1210
  });
2084
1211
  const vAppend = (type, data) => verifySink.append(type, data);
2085
1212
  const verifyTailContentMode = resumeTaskConfig.forwardSubagentEvents === true ? "on" : "progress_only";
2086
1213
  const verifyInternals = {
2087
- // 活性打点:这条臂没有事件循环(stream 臂的 `recordTurnActivity` 在 for-await 里),
2088
- // 不接 ⇒ 一条长跑的 verify-resume 腿在活性面上从 suspend 那刻起就冻住。
2089
1214
  ...(taskId ? { onActivity: () => recordTurnActivity(taskId) } : {}),
2090
1215
  onForwardEvent: (e) => {
2091
1216
  fleetPub?.onForwardEvent(e);
@@ -2100,32 +1225,15 @@ export function createHttpServer(rawDeps) {
2100
1225
  }
2101
1226
  }
2102
1227
  }
2103
- // 三型投影 + 白名单 builder + 单写串行链全在共享 sink 里(见上面的顶注);写失败留痕由
2104
- // sink 自己的 catch 兜(它对失败步不断链)。
2105
1228
  if (rsV && taskId)
2106
1229
  verifySink.onForwardEvent(e);
2107
1230
  },
2108
- // 🔴 **#255(core 5.34.0 #253 / 判据帖 [3967] G1):status 第五席**。这条臂没有事件循环 ——
2109
- // 旁边 resumeStream 臂的顶层 status 走它自己那条 for-await 的 `case "status"`(driveResume
2110
- // switch),本臂在 5.33 时代整段重试期是**黑的**(座位那时还不存在)。core 5.34.0 的
2111
- // `statusEmit` 双写(queue 照旧 + 座位喂**同一个** frozen 帧)把这条腿补齐,这里接的是
2112
- // **同一条**投影链:`brain_retry_total` 计数 + 共享 `verifySink` 的白名单 builder/单写串行链。
2113
- // ⚠️ 帧 frozen ⇒ 这条链上不许写帧字段;同步回调不能 await ⇒ fire-and-forget。下面那个
2114
- // `.catch` 买的是**留痕**不是进程安全:`appendStatus` 返回的链步在 `LedgerSink.append` 内部
2115
- // 已被挂上处理器(「失败不断链」)⇒ 没有逃逸的 rejection(实测:摘掉它 §4⑥b 仍绿);
2116
- // 判别力由 §4⑥b 的「诊断行在场」断言看守(与 `src/runs.ts` 的 `statusSeat` 同款注)。
2117
- // ⚠️ 与 `src/runs.ts` 的 `statusSeat` 同一条纪律:**只**挂在「只回结果」的这条臂上,
2118
- // 旁边 resumeStream 臂那只 internals 不带它(带了就是同一帧两行账本 + 计数翻倍)。
2119
- // 🔴 走 sink 的 **`appendStatus` 同步入链门**,不是 `onEvent` —— 后者先 `await flush()`
2120
- // 才入链,同一拍后到的 `onForwardEvent`(同步入链)会抢到更小的序号,账本时间线倒挂
2121
- // (codex 对抗复审 R2 [medium],红先复现后修;本臂与 `src/runs.ts` 的 `statusSeat` 同病同修)。
2122
1231
  onStatusEvent: (e) => {
2123
1232
  deps.metrics?.inc("brain_retry_total", { phase: String(e.phase) });
2124
1233
  if (rsV && taskId) {
2125
1234
  void verifySink.appendStatus(e).catch((err) => deps.logger?.warn?.("status_event_append_failed", { route: "resume-verify", taskId, err: err instanceof Error ? err.message : String(err) }));
2126
1235
  }
2127
1236
  },
2128
- // bg 子代终态:与 resumeStream 臂逐字同款(理由全在那份孪生注里)。
2129
1237
  onTaskNotification: (n) => {
2130
1238
  if (n.task_type === "workflow")
2131
1239
  return;
@@ -2146,7 +1254,6 @@ export function createHttpServer(rawDeps) {
2146
1254
  }
2147
1255
  }
2148
1256
  },
2149
- // 子代 steer 句柄登记:与 resumeStream 臂同款(腿末尾统一撤,见 finally)。
2150
1257
  ...(deps.subagentSteerRegistry && taskId
2151
1258
  ? {
2152
1259
  onSubagentSpawn: (handle) => {
@@ -2155,122 +1262,64 @@ export function createHttpServer(rawDeps) {
2155
1262
  }
2156
1263
  : {}),
2157
1264
  };
2158
- // resumeWithVerification runs the SAME pre-CAS guard + atomic CAS as resume() internally, so a lost
2159
- // CAS rejects with CheckpointError before any work handled by the catch below, same as the stream path.
2160
- resumeLegLive = true; // 与 stream 臂同义:腿在跑 ⇒ bg 终态走 live 落账而非 durable park(finally 清)
2161
- const vr = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals); // 快审 F1:整对象直传;codex F3:带 taskId 的形
1265
+ resumeLegLive = true;
1266
+ const vr = await resumeWithVerification(legRunner, token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, resumeObjective, verifyRounds, verifyInternals);
2162
1267
  let safe = stripCheckpointToken(vr);
2163
- // 🔴 A re-suspend on a LATER gate is NOT terminal: core maps it to status:"failed" +
2164
- // verification.unverifiedReason with the checkpoint token still live. Treat it exactly like
2165
- // the stream path's park branches — write the matching park event and return the park status so the
2166
- // outer block calls setSuspended/setNeedsReview (keeps the session lock + resumable row). Driving it
2167
- // terminal would release the lock and orphan the second checkpoint (next /decide → 409, task abandoned).
2168
- //
2169
- // 🔴 park 是**两个词**(扫描P2,status/kind 同名词混淆病族),而且判据必须取 **errorCode** 而不是
2170
- // `verification.unverifiedReason` —— 完整理由(含修复轮那条 reason 恒缺席的腿)见
2171
- // {@link nestedParkStatusOf} 的顶注,它是这条判据的唯一属主。只认 `unverifiedReason === "suspended"`
2172
- // 会漏掉两类真 park:plan_review / dry_run 的 `needs_review` 再停,以及 verifier 判 FAIL 后修复轮
2173
- // 的**任一**再停 —— 两类都会被写成 `done` + setTerminal:锁被释放、活卡成孤儿。
2174
- // ⚠️ `verifier_paused` 不进这个集合:那是**验证器自己**的会话/凭据(见 core `VerificationOutcome
2175
- // .verifierPause`),本 run 的活已经做完,与「本 run 再停在自己的门上」是两件事。
2176
1268
  const reParkStatus = nestedParkStatusOf(vr);
2177
1269
  const reSuspended = reParkStatus !== undefined;
2178
- // [1245] codex-4: reopen-class twin of the stream path (store-confirmed, codex-5) — the checkpoint
2179
- // is pending again, so the durable log must record a park, never `done` (the outer row drive
2180
- // re-parks via checkpointReopened; the response keeps status:"failed"+retriable, unlike reSuspended
2181
- // which surfaces "suspended").
2182
1270
  const vrReopened = isCheckpointReopenedFailure(vr) && (await confirmCheckpointReopened());
2183
1271
  if (vrReopened)
2184
1272
  reopenConfirmedPark = true;
2185
1273
  if (taskId && deps.runStore) {
2186
1274
  const rs2 = deps.runStore;
2187
- // codex-6/7 (vrReopened leg): EVERYTHING here is observability — best-effort once the reopen is
2188
- // confirmed (maxSeq + manifest + usage + suspended event): a thrown append must not rethrow past
2189
- // the authoritative row re-park below (stream twin). reSuspended-only keeps its fail-loud shape.
2190
1275
  try {
2191
- // #253:序号来自腿开跑前铸的**同一个**分配器(`vseq`/`vChain`),不再在这里二次 `maxSeq()`
2192
- // ——腿运行期间 forward sink 可能已经写过行,二次现读会与那些 fire-and-forget 写撞号。
2193
1276
  const ap = vAppend;
2194
- await appendPromptManifest(ap, deps.promptManifests, taskId); // [998]②: the verified-resume leg has no stream — persist the re-fired manifest before its terminal/suspend event
1277
+ await appendPromptManifest(ap, deps.promptManifests, taskId);
2195
1278
  if (reSuspended || vrReopened) {
2196
- await appendModelUsageDelta(ap, deps.modelUsage, taskId); // E8: persist pre-re-suspend per-model usage
2197
- // reopen 腿恒落 `suspended`(它不是 gate 再停,是同一张卡被重开);reSuspended 腿按真 park 词落名。
2198
- // [4913]:再停事件带待批 toolCallId(源=新 checkpoint 的 pendingAction;tool-less/读失败=键缺席)。
1279
+ await appendModelUsageDelta(ap, deps.modelUsage, taskId);
2199
1280
  const vrParkCallId = reSuspended ? await parkToolCallId(vr, deps.checkpointStore) : undefined;
2200
1281
  await ap(reSuspended ? reParkStatus : "suspended", { gate: vr.checkpointGate ?? null, ...(vrParkCallId !== undefined ? { toolCallId: vrParkCallId } : {}), ...(vrReopened ? { reopened: vr.errorCode ?? null } : {}) });
2201
1282
  }
2202
1283
  else {
2203
- // E8: this verified-resume leg is the run's final done → flush the last delta + SUM model_usage into stats.modelUsage
2204
1284
  safe = await attachModelUsage(safe, { append: ap, getEvents: (id, a) => rs2.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
2205
- await ap("done", { result: safe }); // verify is not a live stream → one terminal event
1285
+ await ap("done", { result: safe });
2206
1286
  }
2207
1287
  }
2208
1288
  catch (e) {
2209
1289
  if (!vrReopened)
2210
- throw e; // best-effort ONLY on the confirmed-reopen leg — see the boundary note above
1290
+ throw e;
2211
1291
  }
2212
1292
  }
2213
1293
  if (reSuspended)
2214
1294
  return { ...safe, status: reParkStatus };
2215
- // Count the gate verdict only when the verifier actually ran (a re-suspend never reached it).
2216
1295
  if (vr.verification?.verdict)
2217
1296
  deps.metrics?.inc("verifications_total", { verdict: vr.verification.verdict });
2218
1297
  return safe;
2219
1298
  }
2220
- // resumeStream runs the SAME pre-CAS guard + atomic CAS as resume(), then returns a live stream; a
2221
- // lost CAS rejects (CheckpointError) BEFORE any stream → never re-runs a resolved action.
2222
- // E23: core's resumeStream is EAGER (runtask.js:1710 — once it resolves the resumed run is already
2223
- // executing), so the per-run elicitation context MUST exist BEFORE the stream is created, or an inbound MCP
2224
- // elicitation early in the resumed leg finds no ALS store and fail-closed declines. Hoist the durable-append
2225
- // machinery, then create + drain the stream INSIDE runWithContext (emit = append → events tail). The owner is
2226
- // the ORIGINAL submitter (`principal` = the checkpoint scope), NOT the operator who /decided — they answer.
2227
1299
  const rs = deps.runStore;
2228
1300
  const startSeq = rs && taskId ? await rs.maxSeq(taskId) : 0;
2229
1301
  let text = "";
2230
1302
  let reasoning = "";
2231
1303
  const persistThinking = deps.config.traceThinking;
2232
- // 🔴 **单写串行链**(#310 codex 对抗复审 R1-[high],验真后修)。此前本腿是裸写口
2233
- // (`rs.appendEvent(taskId, ++seq, …)`,同步分配 seq、立刻发起 insert)—— 主循环 await 自己的写
2234
- // 所以主循环内部有序,但本腿有**两族 fire-and-forget 写者**:子代 forward 帧(下方 `warnAppend`
2235
- // 那三处,既有)与 #310 的通告口(新增)。它们的 N 号 insert 在池化 SQL 后端上可能晚于后发的
2236
- // N+1 号提交,而 events tail 读 `getEvents(id, afterSeq)`:读到 N+1 就把游标推过 N,那一行
2237
- // **此后永远不会被投递**(重连也不会,游标只前进)。bg / sync 两腿早就有这条链(`LedgerSink`
2238
- // 顶注 [1.211 codex H2] 逐字记着同一个病),本腿是漏掉的第三条 —— 判据抽成共享件,三腿同源。
2239
1304
  const serialAppend = createSerialLedgerAppend((s, type, data) => rs.appendEvent(taskId, s, type, data), startSeq);
2240
1305
  const append = (type, data) => (rs && taskId ? serialAppend(type, data) : Promise.resolve());
2241
- // #310:本腿同样**没有 live SSE**(续跑的消费方走 `GET /v1/runs/:id/events` 的 durable tail),
2242
- // 故只挂 durable 口 —— 与本腿的 elicit/question/approval 三族帧姿态逐字相同。无账本(无 rs/taskId)
2243
- // 时 `append` 本就是 resolve() 空写,注册它不产生任何行(与本腿其它帧同一条退化路径)。
2244
1306
  unregisterEngineNotice = registerEngineNoticeLeg({
2245
1307
  sessionId,
2246
- // 写失败留痕不裸吞(codex 对抗复审 R1-[medium]):本腿账本同样是通告的唯一用户可见终点,
2247
- // 与下方子代 forward 写口的 `warnAppend` 同一条纪律(C2/C5 批2)。
2248
1308
  durable: (row) => void append("engine_notice", row).catch((err) => {
2249
1309
  recordFailOpen("server.engine-notice.durable-append-failed", `leg=resume code=${row.code} task=${taskId ?? ""}`);
2250
- // 字段名是 `noticeCode` 不是 `code`:`error-code-key-gate` 按词法扫 http/ 全树的 `code:` 键
2251
- // (3.0.0 起 wire 错误体只认 `errorCode`),一条日志行也不给它留歧义面。
2252
1310
  deps.logger?.warn?.("engine_notice_append_failed", { taskId, noticeCode: row.code, sessionId: row.sessionId, err: err instanceof Error ? err.message : String(err) });
2253
1311
  }),
2254
1312
  });
2255
- // (resumeNotifiedKeys — this leg's delivered-notification keys — is hoisted above the inbox drain, see top)
2256
- // E18: anchor POST-resume turns too (a resumed conversation's new messages are rewindable). Same capture as
2257
- // runInBackground via the SHARED TurnAnchorCapture (only a settled assistant-text turn is anchored — see its
2258
- // doc). owner = the original submitter (the checkpoint scope), not the operator who /decided. undefined
2259
- // captureTurnAnchor ⇒ no anchor store / no getLeafId.
2260
1313
  const anchorOwner = principal ?? null;
2261
1314
  const getLeafId = deps.sessionStorage?.getLeafId?.bind(deps.sessionStorage);
2262
1315
  const captureTurnAnchor = deps.resumeAnchorStore && getLeafId
2263
1316
  ? async (eventId) => { const leaf = await getLeafId(sessionId); if (leaf)
2264
1317
  await deps.resumeAnchorStore.put(sessionId, eventId, leaf, anchorOwner); }
2265
1318
  : undefined;
2266
- // R8 (CC-parity rewind): USER-message anchor keyed by taskId (parity with the first-run path). A resume leg
2267
- // rarely commits a new user message, but wiring it is harmless (only `message_committed{role:"user"}` fires it).
2268
1319
  const captureUserMessageAnchor = deps.resumeAnchorStore && taskId
2269
1320
  ? async (entryId) => { await deps.resumeAnchorStore.put(sessionId, taskId, entryId, anchorOwner); }
2270
1321
  : undefined;
2271
1322
  const anchor = new TurnAnchorCapture(captureTurnAnchor, () => deps.metrics?.inc("resume_anchor_capture_failed"), captureUserMessageAnchor);
2272
- // Parity with runInBackground (src/runs.ts): the resume leg must capture thinking + per-turn usage too,
2273
- // else a suspend→resume loses them on post-resume turns. Reasoning flushed before content, redacted.
2274
1323
  const flush = async () => {
2275
1324
  if (reasoning) {
2276
1325
  await append("reasoning", { text: redactSecrets(reasoning) });
@@ -2279,31 +1328,17 @@ export function createHttpServer(rawDeps) {
2279
1328
  if (text) {
2280
1329
  await append("text", { text, ...(anchor.firstTextEventId ? { eventId: anchor.firstTextEventId } : {}) });
2281
1330
  text = "";
2282
- } // E18: per-message resume handle
1331
+ }
2283
1332
  };
2284
1333
  const driveResume = async () => {
2285
- // (the workflow-completion inbox drain for this leg is hoisted ABOVE the verify/stream fork — see top)
2286
- // core 1.210: the resume leg now has the RunInternals seam — fan a subagent's task_progress
2287
- // tick (child-isolated stream; the forward sink is its ONLY exit) out to the fleet AND the durable log
2288
- // (shared whitelist+redact builder + E2 identity), parity with the sync/bg legs. Fire-and-forget append
2289
- // (`++seq` is a sync increment — sink-vs-loop appends get unique seqs); only when a durable log exists.
2290
- // false once this leg settles — notifications after that take the durable-inbox path.
2291
1334
  resumeLegLive = true;
2292
- // [3911] 定谳(bg/sync 两腿 twin):per-agent tail 的**内容帧供给**由本腿 spec 的
2293
- // forwardSubagentEvents 决定(core prepare-task 的 forward 门)。逐帧按**句柄**登记
2294
- // (键为什么是句柄:SubagentTailBus.modeByHandle 顶注)。⚠️ 旧注这里写的「verify 腿不传
2295
- // internals ⇒ 无帧 ⇒ meta 出 unknown」自 #253(core 5.33.0 #246)起**已过期**:verify 臂
2296
- // 现在也传 internals(见上面那段),两条 resume 臂的供给形因此同形。
2297
1335
  const tailContentMode = resumeTaskConfig.forwardSubagentEvents === true ? "on" : "progress_only";
2298
1336
  const stream = await legRunner.resumeStream(token, outcome, { ...resumeTaskConfig, signal: cancelCtrl.signal, preemptSignal: preemptCtrl.signal }, {
2299
1337
  onForwardEvent: (e) => {
2300
1338
  fleetPub?.onForwardEvent(e);
2301
- // S2 live tail(复审 #1:forward sink 有三条腿——resume 腿上 spawn 的 bg 子代同样带
2302
- // bgAgentId,漏挂=同副本开流只见 heartbeat)。publish 在 rs/taskId 门之前(tail 独立于账本)。
2303
1339
  {
2304
1340
  const bg = e.bgAgentId;
2305
1341
  if (bg !== undefined) {
2306
- // 登记在 hasSubscribers 快门**之前**:没人在听时也要学,否则晚开的 tail 读不到供给形。
2307
1342
  defaultSubagentTailBus.noteHandleContentMode(bg, tailContentMode);
2308
1343
  if (defaultSubagentTailBus.hasSubscribers(bg)) {
2309
1344
  const f = projectTailFrame(e);
@@ -2315,60 +1350,37 @@ export function createHttpServer(rawDeps) {
2315
1350
  if (!rs || !taskId)
2316
1351
  return;
2317
1352
  const t = e.type;
2318
- // C2/C5(v3.1 批2):写失败留痕——同函数 onTaskNotification 的 park-enqueue 早按 2026-07-11
2319
- // 对抗评审补了 warn(false park 指纹),这三处同构 fire-and-forget 此前裸吞:子代 tool 生命周期
2320
- // 事件从 durable log 永久消失且零信号。结构上仍不能 await(同步回调),留痕不改调用形。
2321
1353
  const warnAppend = (kind) => (err) => deps.logger?.warn?.("forward_event_append_failed", { type: kind, taskId, err: err instanceof Error ? err.message : String(err) });
2322
1354
  if (t === "task_progress") {
2323
1355
  void append("task_progress", taskProgressEventData(e)).catch(warnAppend(t));
2324
1356
  }
2325
1357
  else if (t === "tool_start") {
2326
- // C1 (core 1.219): a delegated child's forwarded tool lifecycle — durable via the SAME shared
2327
- // whitelist+redact builders as the top stream (parity with the bg leg; deltas not persisted per-chunk).
2328
1358
  void append("tool_start", toolStartEventData(e)).catch(warnAppend(t));
2329
1359
  }
2330
1360
  else if (t === "tool_end") {
2331
1361
  void append("tool_end", toolEndEventData(e)).catch(warnAppend(t));
2332
1362
  }
2333
1363
  },
2334
- // background-completion observer — flip the child's fleet row +
2335
- // park an idle-arrival in the durable inbox (workflow-type skipped). Rationale in runs.ts (bg twin).
2336
- // core 1.283 `seq` (per-stop-cycle counter) rides into the dedup key + parked entry — sync twin's note.
2337
- // 白名单排查(2026-07-24,「彻查 as {} 连环坑」):手写窄类型换 core 真实类型,理由同 runs.ts twin。
2338
1364
  onTaskNotification: (n) => {
2339
1365
  if (n.task_type === "workflow")
2340
1366
  return;
2341
- // S2 复审 #3(sync 腿同注):中途终态信号。
2342
- // [3911] 终态即撤供给形登记:同一 a* 句柄可进入下一个**复活周期**,而新周期的宿主腿模式
2343
- // 可能相反——不撤会把上一周期的答案报给新周期的首帧前窗口(理由全文见 forgetHandleContentMode)。
2344
1367
  defaultSubagentTailBus.forgetHandleContentMode(n.task_id);
2345
1368
  if (defaultSubagentTailBus.hasSubscribers(n.task_id)) {
2346
1369
  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) } : {}) });
2347
1370
  }
2348
- // id-domain alias: flip by payload.sessionId (= the tick's uuid domain), fallback task_id.
2349
- // [2687-cli] 幽灵行案:只对 agent 族终态打(isFleetAgentTerminalNotification 单源判别,
2350
- // 病灶链见其 doc 注);bash/monitor 的通知帧/park 面照走。孪生:runs.ts bg 腿、routes/tasks.ts sync 腿。
2351
1371
  const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
2352
1372
  const parked = !resumeLegLive && Boolean(deps.workflowCompletionInbox && sessionId);
2353
- // diagnosability (rationale in runs.ts twin).
2354
1373
  deps.logger?.info?.("task_notification_observed", { route: "resume", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: resumeLegLive, parkedDurable: parked });
2355
1374
  if (parked) {
2356
- // 对抗评审 2026-07-11(MED):log the enqueue failure (false park✓ fingerprint otherwise; sync twin's note).
2357
1375
  void deps.workflowCompletionInbox.enqueue(taskNotificationInboxEntry(sessionId, principal ?? null, n, Date.now(), taskId)).catch((err) => deps.logger?.warn?.("park_enqueue_failed", { route: "resume", taskId: n.task_id, err: err instanceof Error ? err.message : String(err) }));
2358
1376
  }
2359
1377
  else if (resumeLegLive) {
2360
- // option A: teardown-time reap notification → durable display frame NOW (bg-leg twin's note).
2361
- // core [784]④a: seq-folded shared key — the seq-less shape reverse-swallowed cycle 2's frame.
2362
- // codex F1 (HIGH): fire-and-forget append — claim the key with its REAL outcome so a failed
2363
- // write doesn't eat the stream replay of the same key (bg twin's note).
2364
1378
  const key = taskNotificationStreamKey(n);
2365
1379
  if (resumeNotifiedKeys.get(key) === undefined) {
2366
1380
  resumeNotifiedKeys.set(key, append("task_notification", taskNotificationEventData({ notification: n })).then(() => true, () => false));
2367
1381
  }
2368
1382
  }
2369
1383
  },
2370
- // C2 (core 1.219) + design/122 (core 1.225 ③): the resumed leg re-registers sync-delegation steer
2371
- // handles under the SAME taskId. NOT evicted on settle (resume legality) — evicted at leg end.
2372
1384
  ...(deps.subagentSteerRegistry && taskId
2373
1385
  ? {
2374
1386
  onSubagentSpawn: (handle) => {
@@ -2379,29 +1391,19 @@ export function createHttpServer(rawDeps) {
2379
1391
  });
2380
1392
  resumeStreamRef = stream;
2381
1393
  if (taskId)
2382
- steerableRuns.set(taskId, stream); // the resumed leg is in-flight again → live-steerable (parity with the first-run plain-stream path; taskId-guarded like the finally)
1394
+ steerableRuns.set(taskId, stream);
2383
1395
  if (!taskId || !rs)
2384
- return await stream.result(); // no durable log to persist into → just drain
2385
- // ── #316 **受理点**([4660] Inkglow 60s 雪崩首环的修)──────────────────────────────────
2386
- // 到这一行为止,所有「会变成拒绝」的判定都已经落定:lease admission(429)、markResuming CAS
2387
- // (409)、以及 `resumeStream` 内部 core 的 pre-CAS 守卫 + 原子 CAS(绑定不符 409 / 卡不在 404
2388
- // ——它们在 `await legRunner.resumeStream(...)` 处就 reject 了,根本到不了这里)。剩下的只有
2389
- // **模型往返**,时长无上限。⇒ 声明了受理语义的调用方在此拿走 `200 {status:"resuming"}`,本腿
2390
- // 继续在同一条 promise 链上跑到停点(账本写口逐字还是上面那条 `createSerialLedgerAppend`
2391
- // 单写串行链 —— 本改动**不新增任何写口**,也就不引入 fire-and-forget 的低 seq 竞态)。
2392
- // 🔴 `taskId && rs` 之后才受理(上一行的早退已经把无行形滤走):没有 durable 行就没有 poll/SSE
2393
- // 可跟的终态面,那种部署形上提前受理等于把结果扔掉 —— 它维持同步形(受理回调不触发 ⇒ 分派器
2394
- // 的 race 自然落到腿本身的返回值上)。
1396
+ return await stream.result();
2395
1397
  onAccepted?.({ status: 200, body: { taskId, sessionId, status: "resuming", bindingEnforced: true } });
2396
1398
  for await (const ev of stream) {
2397
- recordTurnActivity(taskId); // codex F5:S1 活性打点网的 resume 腿(sync/bg 双腿在 ledger-sink onActivity,本腿 append 不走那条链)——漏打则 resume 后 msSinceLastActivity 冻在 suspend 前,活跑的 run 被读成僵死
2398
- fleetPub?.onEvent(ev); // MF-Fleet (#7): turn_end → tokens, tool_start → live activity, in-stream task_progress → child row (parity with the sync leg's onEvent)
2399
- await appendPromptManifest(append, deps.promptManifests, taskId); // [998]②: the resumed prepare's manifest lands ahead of its first event (runs.ts twin)
1399
+ recordTurnActivity(taskId);
1400
+ fleetPub?.onEvent(ev);
1401
+ await appendPromptManifest(append, deps.promptManifests, taskId);
2400
1402
  switch (ev.type) {
2401
1403
  case "text_delta":
2402
1404
  text += ev.delta;
2403
1405
  anchor.onText(ev.eventId);
2404
- break; // E18: latch first-text eventId = message handle
1406
+ break;
2405
1407
  case "reasoning_delta":
2406
1408
  if (persistThinking)
2407
1409
  reasoning += ev.delta;
@@ -2410,7 +1412,7 @@ export function createHttpServer(rawDeps) {
2410
1412
  anchor.onTool();
2411
1413
  await flush();
2412
1414
  await append("tool_start", toolStartEventData(ev));
2413
- break; // E18: tool ran → not anchorable
1415
+ break;
2414
1416
  case "tool_end":
2415
1417
  await flush();
2416
1418
  await append("tool_end", toolEndEventData(ev));
@@ -2421,78 +1423,69 @@ export function createHttpServer(rawDeps) {
2421
1423
  await flush();
2422
1424
  await append("status", brainStatusEventData(st));
2423
1425
  break;
2424
- } // S4 P0-a: durable brain-liveness observation (rationale in runs.ts twin — flush first so a mid-text reconnect doesn't invert the timeline); design/158 B2: row type aligned to the live frame name ("status", was "brain_status" — ledger-sink.ts twin's note has the full rationale + compat contract); 白名单排查 2026-07-24: 手写字面量换 Parameters<> 防未来漂移
1426
+ }
2425
1427
  case "context_usage":
2426
1428
  await flush();
2427
1429
  await append("context_usage", contextUsageEventData(ev));
2428
- break; /* core 1.414 一等分支;两条口径见 builder 旁注 */
1430
+ break;
2429
1431
  case "compacted":
2430
1432
  await flush();
2431
1433
  await append("compacted", compactedEventData(ev));
2432
- break; // MF-18 trigger + core 1.219 attachedFiles/preserved_segment (shared whitelist builder)
1434
+ break;
2433
1435
  case "diagnostics":
2434
1436
  await flush();
2435
1437
  await append("diagnostics", diagnosticsEventData(ev));
2436
- break; // core 1.220 design/121: NEW LSP diagnostics (shared whitelist builder, §E1 redact)
1438
+ break;
2437
1439
  case "steering_injected":
2438
1440
  await flush();
2439
1441
  await append("steering_injected", steeringInjectedEventData(ev));
2440
- break; // [1550]/[1552]: shared whitelist builder, §E1 redact — resume twin of the sync + ledger-sink legs
1442
+ break;
2441
1443
  case "compaction_outcome":
2442
1444
  await flush();
2443
1445
  await append("compaction_outcome", compactionOutcomeEventData(ev));
2444
- break; // [2373]A-2/C-2: durable 双腿补齐(此前回放即消失);共享 builder,差集门看守
1446
+ break;
2445
1447
  case "workspace_changed":
2446
1448
  await flush();
2447
1449
  await append("workspace_changed", workspaceChangedEventData(ev));
2448
- break; // [1559]二: cwd move (bash cd/EnterWorktree) — shared whitelist builder, §E1 redact — resume twin of the sync + ledger-sink legs
1450
+ break;
2449
1451
  case "wiring_manifest":
2450
1452
  await flush();
2451
1453
  await append("wiring_manifest", wiringManifestEventData(ev));
2452
- break; // 🔴 core 5.14.0 design/173:resume 腿自证清单(每条腿发自己那份)。本腿与 durable 账本腿共用**同一个**租户面构造器,剥 `governance` 段(audience:"operator",no projection ⇒ do not disclose)——账本写时不知道未来读者是谁,存全量 = 把 operator-only 面写进租户可读介质。⚠️ 「三腿同源」自 #154 件一起不再成立:live sync 腿(routes/tasks.ts)按连接身份分叉,operator 连接走 `wiringManifestOperatorEventData`。所以判据是「**durable 两腿恒剥、live 腿按身份分叉**」,不是「分头挑键即泄露」——别照旧注把已落地的 operator 面当泄露删掉,也别给第四条腿抄错构造器:先问这条腿知不知道读者是谁
1454
+ break;
2453
1455
  case "human_input":
2454
1456
  await flush();
2455
1457
  await append("human_input", humanInputEventData(ev));
2456
- break; // core 5.14.0 design/171:人类输入生命周期账本(parked-steer 续跑帧正是在这条腿上出现);身份串脱敏在共享构造器里
1458
+ break;
2457
1459
  case "message_committed":
2458
1460
  await anchor.onMessageCommitted(ev.role, ev.entryId);
2459
- break; // R8: USER-message rewind anchor (CC "rewind to the prompt")
1461
+ break;
2460
1462
  case "task_progress":
2461
1463
  await append("task_progress", taskProgressEventData(ev));
2462
- break; // shared whitelist+redact builder (+E2 identity, 1.78) — parity with the runs.ts + sync legs
1464
+ break;
2463
1465
  case "task_notification": {
2464
1466
  await flush();
2465
- // core [784]④a+④b: seq-folded shared key + skip keys this leg already delivered (turn-open inbox
2466
- // drain or observer arm) — core's pend-drain replays a double-parked idle arrival as a stream event
2467
- // on the resumed leg; appending it again doubles the frame in the durable events tail (sync twin).
2468
- // codex F1: await the claiming write's outcome — a failed observer claim must not eat this
2469
- // replay (write anyway + promote the key with THIS write's outcome; bg twin's note).
2470
1467
  const tnr = ev.notification;
2471
1468
  const tnrKey = tnr?.task_id ? taskNotificationStreamKey({ task_type: tnr.task_type, task_id: tnr.task_id, status: String(tnr.status), seq: tnr.seq }) : undefined;
2472
1469
  const tnrPrior = tnrKey !== undefined ? resumeNotifiedKeys.get(tnrKey) : undefined;
2473
1470
  if (tnrPrior === undefined || !(await tnrPrior)) {
2474
- const write = append("task_notification", taskNotificationEventData(ev)); // core 1.202 design/115 P2: display projection (shared whitelist+redact builder — parity with the runs.ts + sync legs)
1471
+ const write = append("task_notification", taskNotificationEventData(ev));
2475
1472
  if (tnrKey !== undefined)
2476
- resumeNotifiedKeys.set(tnrKey, write.then(() => true, () => false)); // stream delivered this key
1473
+ resumeNotifiedKeys.set(tnrKey, write.then(() => true, () => false));
2477
1474
  await write;
2478
1475
  }
2479
1476
  break;
2480
1477
  }
2481
1478
  case "turn_end": {
2482
1479
  await flush();
2483
- await append("turn_end", turnEndEventData(ev)); /* 白名单 builder:此前手写 {usage},剥掉了 stopReason/usageMissing */
1480
+ await append("turn_end", turnEndEventData(ev));
2484
1481
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
2485
1482
  await anchor.onTurnEnd();
2486
1483
  break;
2487
- } // E18: anchor a settled assistant-text turn only
2488
- // "done" handled below via result() + the run-row drive (terminal event written once)
1484
+ }
2489
1485
  }
2490
1486
  }
2491
1487
  await flush();
2492
1488
  const r = await stream.result();
2493
- // Close the durable stream with the matching terminal event (mirrors runInBackground). Never the
2494
- // capability token — only the non-secret gate on a re-suspend.
2495
- // [4913]:再停事件带待批 toolCallId(源=新 checkpoint 的 pendingAction;tool-less/读失败=键缺席)。
2496
1489
  if (r.status === "suspended") {
2497
1490
  const parkCallId = await parkToolCallId(r, deps.checkpointStore);
2498
1491
  await append("suspended", { gate: r.checkpointGate ?? null, ...(parkCallId !== undefined ? { toolCallId: parkCallId } : {}) });
@@ -2503,34 +1496,19 @@ export function createHttpServer(rawDeps) {
2503
1496
  await append("needs_review", { gate: r.checkpointGate ?? null });
2504
1497
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
2505
1498
  return r;
2506
- } // D-B: a resumed plan_review re-gated to another review pause
2507
- // [1245] codex-4: a CONFIRMED reopen-class failure (core reopened the checkpoint — store-verified,
2508
- // codex-5) is NOT terminal: logging `done` here would make the append-only log say terminal while
2509
- // the park stays decidable (trace projection + events tailers treat `done` as final and would miss
2510
- // the retry). Log the park, keep E8's terminal usage-SUM reserved for genuinely terminal results
2511
- // (`checkpoint.reopen_failed` and an unconfirmed reopen stay on the done leg).
1499
+ }
2512
1500
  if (isCheckpointReopenedFailure(r) && (await confirmCheckpointReopened())) {
2513
1501
  reopenConfirmedPark = true;
2514
- // codex-6: best-effort — the ROW re-park below is the authority once the reopen is confirmed. A
2515
- // thrown append here would otherwise rethrow past the row drive and strand a `running` row (no
2516
- // heartbeat) over a pending park until reapStale; a missing suspended event is merely a log gap
2517
- // (nothing terminal was written — tailers keep waiting), strictly recoverable.
2518
1502
  try {
2519
1503
  await append("suspended", { gate: null, reopened: r.errorCode ?? null });
2520
1504
  await appendModelUsageDelta(append, deps.modelUsage, taskId);
2521
1505
  }
2522
1506
  catch {
2523
- /* row drive is authoritative */
2524
1507
  }
2525
1508
  return r;
2526
1509
  }
2527
- // E8: terminal this resumed leg is the run's final done, so flush the last delta + SUM model_usage across
2528
- // ALL legs (pre-suspend + post-resume) into stats.modelUsage. setTerminal (below, ~2619) stores this result.
2529
- const safe = await attachModelUsage(stripCheckpointToken(r), { append, getEvents: (id, a) => rs.getEvents(id, a), modelUsage: deps.modelUsage, taskId }); // structural fence (strip no-op today)
1510
+ const safe = await attachModelUsage(stripCheckpointToken(r), { append, getEvents: (id, a) => rs.getEvents(id, a), modelUsage: deps.modelUsage, taskId });
2530
1511
  await append("done", { result: safe });
2531
- // E12 (shell-host contract): a resumed leg that COMPLETES can also emit post-completion suggestions (parity with the
2532
- // first-run path + the completed-only gate). suspended/needs_review returned above; a failed leg is
2533
- // skipped here. Redact + persist as a `suggestions` event (UNTRUSTED UI-only, never re-fed to a model).
2534
1512
  if (taskConfig.suggestNextPrompts && safe.status === "completed") {
2535
1513
  const suggestions = await stream.suggestions().catch(() => []);
2536
1514
  if (suggestions.length)
@@ -2538,17 +1516,10 @@ export function createHttpServer(rawDeps) {
2538
1516
  }
2539
1517
  return safe;
2540
1518
  };
2541
- // #151 车3 刀 3b(design/172 §4.3(b)):resume 腿的**审批 ALS** —— 此前这条腿只有 elicitation ctx,
2542
- // 审批 ask 恒 `"unavailable"` ⇒ 恒 park,一张卡都不产。投递面 = 本腿的 durable events tail
2543
- // (与 bg 腿同形:`append(type, rest)`,`emitCard` 同口;撤卡帧 live-only 故不接)。
2544
- // `legKey` = 上面算好的 token 摘要 —— 这是 resume 腿与 sync/bg 首腿的唯一区别,也正是「新 park ⇒
2545
- // 新腿 ⇒ 重新征询」这条安全语义的承载轴。
2546
1519
  const approvalEmit = (frame) => {
2547
1520
  const { type, ...rest } = frame;
2548
1521
  return append(type, rest);
2549
1522
  };
2550
- // 包 ALS ⟺ 协议在本腿上场且窗不为 0(F4 修;`windowZero` 在没有 per-task `spec.onAsk` 装配点的
2551
- // 腿上,「不包 ALS」就是 immediate-unavailable 闭包的等价形,见 resolveApprovalLeg 顶注)。
2552
1523
  const withApproval = () => deps.toolApproval && taskId && approvalLeg.active && !approvalLeg.windowZero
2553
1524
  ? deps.toolApproval.runWithContext({
2554
1525
  taskId,
@@ -2556,7 +1527,6 @@ export function createHttpServer(rawDeps) {
2556
1527
  emit: approvalEmit,
2557
1528
  abortSignal: cancelCtrl.signal,
2558
1529
  sessionId,
2559
- // 同 bg 腿:唯一 sink 是本腿的 durable tail,append 失败如实算未送达(R2-1)。
2560
1530
  emitCard: createApprovalCardEmitter({ appendDurable: (f) => approvalEmit(f) }),
2561
1531
  legKey,
2562
1532
  ...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}),
@@ -2569,166 +1539,68 @@ export function createHttpServer(rawDeps) {
2569
1539
  }
2570
1540
  catch (e) {
2571
1541
  if (e instanceof CheckpointError) {
2572
- // ── A-010.2:`checkpoint.resume_aborted` = 取消终局(既非 terminal-failed,也**不是** retriable)──
2573
- // core 只在 `taskConfig.signal.aborted === true` 时铸这个码(四个铸造点:pre-CAS 入口 / 编辑复裁
2574
- // 竞速两处 / consumed 后 / reopen 成功后),而本腿递进去的 `signal` 只有 `cancelCtrl.signal` ——
2575
- // 其唯一 abort 源是取消(inflightRuns 句柄 + 心跳轮询的持久取消旗)。所以收到该码 ⟺ 这条 run
2576
- // 被取消了,四形的正确落点因此**同一个**:按取消结算(下面那道 pending/consumed 的判别式,在
2577
- // 「该做什么」这个问题上是无差别的,故不做)。
2578
- // · 归下面的 terminal 支(修前现状)= 200 的取消出路(本 catch 更下面的 `cancelCtrl.signal.aborted`
2579
- // 分支)被本 CheckpointError 分支**遮蔽**:运维拿到 409 + core 裸报文,行虽落 failed 但
2580
- // `errorCode` 不是 `cancelled` 且 `setTerminal` 连 TaskResult 都不写(null),pre-CAS 三形的
2581
- // 卡还留 pending = 行终局 + 卡 pending 的孤儿对。
2582
- // · 归 retriable(把该码加进下面的闭集)= 把一条**用户已取消**的 run 重新 park 成 `suspended`;
2583
- // 下一次 /decide 的 markResuming 会重置取消旗并把活真跑起来 —— 取消被静默打败,执行车道回归。
2584
- // ⛔ 修法刻意是「只给该码开一条臂」而不是把 `cancelCtrl.signal.aborted` 分支上提到本分支之前:
2585
- // 同在 abort 期铸的 `checkpoint.reopen_failed` 两形(store 拒 reopen = 审批已终局消耗;reopen
2586
- // 在飞失败 = 卡的状态从这里无法证明)带的信息比 "cancelled" 多,上提会把它们折没。
2587
1542
  if (e.code === "checkpoint.resume_aborted") {
2588
- // 序与 /v1/runs/:id/cancel 的 cancelSuspended 逐字同向:**先**按 cancel 语义结算卡,**再**驱行
2589
- // 终局(行终局会释放 task_active;反序会开一个「卡还 pending 而行已死」的窗)。cancel 的卡结算
2590
- // 语义是 expire(≈ deny 的 reaper 终局),不是 resolve —— 取消意味着 run 死掉,deny-RESUME 会
2591
- // 把拒绝喂回模型烧 token。consumed 那一形(core 3894)在这里天然是 no-op:CAS 只翻 pending。
2592
- // ⚠️ 与 `/v1/runs/:id/cancel` 的 `cancelSuspended` **刻意分道**的一点(codex 对抗复审 R2-高2
2593
- // 的处置):那只在 CAS 输掉时会改答 409,因为它站在腿**外面**——输掉意味着「可能有一条 decide
2594
- // 腿正在跑」,它没资格驱行终局。本臂站在腿**里面**:markResuming 已经赢过、这条 run 归我们,
2595
- // 而本码的语义是「它被取消了」。此刻 CAS 输掉只可能是卡已离开 pending(自己那次 resume 消费掉
2596
- // 的 consumed 形,或 reaper 先收了)——两种都意味着**没有活审批留在外面**,行照驱终局才是对的:
2597
- // 不驱才会把会话锁一直攥着(这正是 [868] 那条事故)。故不改结果面,但**必须留痕**。
2598
1543
  const cs2 = deps.checkpointStore;
2599
1544
  if (cs2) {
2600
1545
  try {
2601
1546
  const live = await cs2.get(token);
2602
1547
  if (live?.status === "pending" && !(await cs2.expire(token, live.scope))) {
2603
- // CAS 输了:卡在我们读它与写它之间被别人(自己的 resume / reaper)settle 了。行为面无变化,
2604
- // 但「取消时这张卡到底是谁收的」不留痕就永远查不出来。
2605
1548
  deps.logger?.warn?.("resume_cancel_checkpoint_settle_lost", { taskId, sessionId });
2606
1549
  }
2607
1550
  }
2608
1551
  catch (cancelSettleErr) {
2609
- // 卡结算是尽力而为:行终局才是释放锁的权威写,残留的 pending 卡另有 reaper 车道兜底。
2610
- // 但**必须留痕** —— 静默吞掉的话「取消之后卡为什么还在」在遥测里没有任何线索。
2611
1552
  deps.logger?.warn?.("resume_cancel_checkpoint_settle_failed", {
2612
1553
  taskId, sessionId, err: cancelSettleErr instanceof Error ? cancelSettleErr.message : String(cancelSettleErr),
2613
1554
  });
2614
1555
  }
2615
1556
  }
2616
- // `claimedRow &&` 与下面那道分支**同一个**守卫,不是多余的:驱行终局是有资格才做的写(只有
2617
- // 抢到这条 run 的腿才有资格),而「本码只可能在 markResuming 之后铸」这件事是当前控制流的
2618
- // 巧合、不是结构保证。守卫写出来,这条臂就不依赖那个巧合。
2619
1557
  if (claimedRow && taskId && deps.runStore) {
2620
1558
  const c = { taskId, sessionId, status: "failed", errorCode: "cancelled", errorMessage: e.message, stats: { turns: 0, tokens: 0 } };
2621
1559
  await deps.runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null).catch(() => undefined);
2622
1560
  }
2623
- settleFleet("failed"); // MF-Fleet (#7): 取消结算终局 → 行从 fleet 摘除
1561
+ settleFleet("failed");
2624
1562
  return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
2625
1563
  }
2626
- // We flipped the row to `running` (claimedRow). Two CheckpointError classes diverge here (D-1, core 1.101):
2627
- // • TERMINAL (already_resolved / not_found / gate_mismatch): the checkpoint is
2628
- // consumed or gone — drive the row terminal now (releasing task_active) instead of leaving a zombie
2629
- // `running` row for reapStale to mislabel and hold the session lock for ~runStaleSec. (Original case.)
2630
- // ⚠️ `unsupported_version` USED to sit in this bullet unconditionally (its checkpoint does NOT go
2631
- // away — that was the registered orphan debt). As of #209 / core 5.25.0 it SPLITS on
2632
- // `detail.reason`: a recognised worker-swap word joins the retriable class below; an absent /
2633
- // unrecognised word stays TERMINAL. Read the block under the closed set before touching it.
2634
- // • RETRIABLE PRE-CAS (invalid_outcome = binding mismatch / reopen_revote = env_failed must replay the
2635
- // persisted winner / reopened_concurrently = a concurrent resolve-reopen advanced the rev): the
2636
- // checkpoint STAYS pending and the operator re-fetches + re-/decides. Driving the row terminal would
2637
- // release task_active and ORPHAN the still-pending checkpoint (next /decide → 404). Re-park it
2638
- // `suspended` so the corrected retry can claim it again.
2639
1564
  const retriable = e.code === "checkpoint.invalid_outcome" ||
2640
1565
  e.code === "checkpoint.reopen_revote" ||
2641
1566
  e.code === "checkpoint.reopened_concurrently" ||
2642
- e.code === "resume.parent_constraint_missing" || // 历史复审轴A #4(1.254):core 新增两码同为
2643
- e.code === "resume.parent_constraint_mismatch" || // PRE-CAS(checkpoint 保持 pending)——按本块 doctrine 归 retriable;当前 /decide 面只 resume 顶层 checkpoint 应不可达,前瞻补齐防嵌套 resume 面开放时踩中
2644
- // core 5.22.0(BREAKING「编辑复裁前移 pre-CAS」)闭集加员:审批人**改写**入参时,继承祖先链
2645
- // 的冻结投影先静态复裁 —— 判 deny ⇒ constraint_rejected;链上有 opaque 层/声明 live remainder
2646
- // ⇒ constraint_unprojectable。两者与上面两码同族:PRE-CAS、checkpoint 留 pending 且**同 token
2647
- // 仍可再决**(core 明写 fresh-redecision 语义:照原样批 / deny / 改别的都还点得动)。归 terminal
2648
- // 会释放 task_active 并孤儿化那条仍 pending 的卡 —— 一张还能点的卡当场变死件。
1567
+ e.code === "resume.parent_constraint_missing" ||
1568
+ e.code === "resume.parent_constraint_mismatch" ||
2649
1569
  e.code === "resume.constraint_rejected" ||
2650
1570
  e.code === "resume.constraint_unprojectable" ||
2651
- e.code.startsWith("wake.") || // design/144:wake 拒绝(gate_pending/nothing_to_deliver)全是 PRE-CAS,checkpoint 保持 pending——驱 terminal 会孤儿化仍在等的 park
2652
- // 🟢 **`checkpoint.unsupported_version` 的一臂精确 retriable**(#209 件2,core 5.25.0 提货批
2653
- // [3443];取代 #205 件4② 那条「维持 TERMINAL + 孤儿代价在案」的登记账)。
2654
- //
2655
- // 旧账的形状(留着,因为它解释了为什么这条臂长这样):该码在 core 上有**三个** PRE-CAS 铸点
2656
- // (版本超上限 / org 治理行落到无 `permissionRuleOrg` 的 worker / 带 remote `workspaceHandle` 的行
2657
- // 落到无 `executionEnvFactory` 的 Runner),三形卡一律留 pending 但**可恢复性不同**,而 core 当时
2658
- // 同码同 `detail`(都不带)⇒ 下游只剩「按报文文案猜」或「按 checkpoint 形状反推」两条路,两版修法
2659
- // 各被一轮 codex 对抗复审验伪(blanket-retriable 让无手臂永挂 task_active;按 `gate.realApproval`
2660
- // 分流不健全 —— core 把该位放 gate、`workspaceHandle` 放 state,同一条行可以两样都带)。按宪法
2661
- // 「源头修复,禁下游旁路」全回退,并把判别式列成上游请托([3434] 请托 1)。
2662
- //
2663
- // 请托已兑现:core 5.25.0 起三个铸点各带 `detail.reason`(`runtask.js:3706/3721/3733` 亲读),
2664
- // d.ts 逐字说三者都是「retryable on a newer / factory-wired / org-wired worker」。⇒ 本臂**读词表**
2665
- // (`WORKER_SWAP_REDEEMABLE`,闭集、新词 tsc 红),不做任何形状推断:
2666
- // · 认得的三个词 ⇒ retriable:行重新 park suspended,那条仍 pending 的卡不再被孤儿化
2667
- // (旧账里「行 failed 而卡活着、下一次 /decide → 404、只能等 `terminal_at_ms`」的代价至此销掉)。
2668
- // · 词缺席 / 认不得 ⇒ **维持 TERMINAL**(旧行为逐字保留)。这不是保守的装饰:旧引擎的裸抛与
2669
- // 将来某个我们还没读过的新臂都落在这一支,而把它们默认成 retriable 会把一条**真的没救**的行
2670
- // 永久 park 在 suspended 上攥着 task_active —— 那正是 codex R1-[high] 验伪掉的那一版。
2671
- // 🔴 形③(`env_factory_missing`)按 core 的词判 retriable,而不是按 #205 那条「选 Runner 由
2672
- // `handsLanes.laneOf(spec)` 按场景判,重投永远同样失败」的推断:那条推断说的是**同一份部署配置下**
2673
- // 重投无用,而 core 的词说的是「接上 `executionEnvFactory` 的 worker 兑得掉」—— 那是一次运维动作,
2674
- // 与 `governed_unwired` 要求接 `permissionRuleOrg` 完全同族。两者都不是「换个副本再点一次」。
2675
- // 期间行停在 suspended 攥着会话锁,与它 park 时的状态**逐字相同**(这条腿只是没能把它推进),
2676
- // 而 TERMINAL 那一支要用「释放锁」换「卡变死件」,方向更坏。
2677
- // 行为面钉:`test/durable-resume-http.test.ts` 的「#209 件2」三正格 + 三反极格。
1571
+ e.code.startsWith("wake.") ||
2678
1572
  (e.code === "checkpoint.unsupported_version" && checkpointRowRedeemableElsewhere(e.detail?.reason));
2679
1573
  if (claimedRow && taskId && deps.runStore) {
2680
- // Re-park on the SAME gate family the outcome targeted: a plan_review retry must re-park `needs_review`
2681
- // (keep the row's review status + the lock), not `suspended` — else GET /v1/assistant/tasks mislabels it.
2682
1574
  if (retriable) {
2683
1575
  await (outcome.gate === "plan_review" ? deps.runStore.setNeedsReview(taskId) : deps.runStore.setSuspended(taskId)).catch(() => undefined);
2684
1576
  settleFleet(outcome.gate === "plan_review" ? "needs_review" : "suspended");
2685
- } // MF-Fleet (#7): re-park keeps the row
1577
+ }
2686
1578
  else {
2687
1579
  await deps.runStore.setTerminal(taskId, "failed", null, e.message).catch(() => undefined);
2688
1580
  settleFleet("failed");
2689
- } // MF-Fleet (#7): terminal removes the row
1581
+ }
2690
1582
  }
2691
1583
  const status = e.code === "checkpoint.not_found" ? 404 : 409;
2692
1584
  return {
2693
1585
  status,
2694
1586
  body: {
2695
1587
  error: e.message,
2696
- // invalid_outcome means DIFFERENT things per gate: for policy_ask it IS the D-1 action-binding
2697
- // mismatch; for plan_review/dry_run_review it is a content-sanitize reject (editedPlan/reason carried a
2698
- // forbidden tag) — give those a gate-appropriate code so the UI routes to the right recovery, not the
2699
- // action-binding flow (adversarial-review medium finding).
2700
- // 归一(clay 大改版窗 2026-07-28):errorCode **恒在**——改铸臂保持优先,其余=原码镜像
2701
- // (「code 单轨」历史形从此退役;SDK 的 ?? b.code 兜底只为老 server 保留)。
2702
1588
  errorCode: e.code === "checkpoint.invalid_outcome"
2703
1589
  ? (outcome.gate === "policy_ask" ? "approval_binding_mismatch" : "resume_outcome_invalid")
2704
1590
  : e.code,
2705
- // [2422] HITL-2 裁 (a),core 5.3.0:invalid_outcome 同码两义(动作被换 vs 输入被换)——
2706
- // detail.field 是唯一机读判别式,透传给 UI 路由恢复动作(boundCallId=重取 pending 列表,
2707
- // boundInputHash=重审同动作的新输入)。缺席=core 未铸(非绑定类 invalid_outcome),不造键。
2708
1591
  ...(e.detail?.field ? { field: e.detail.field } : {}),
2709
1592
  ...(retriable ? { retriable: true } : {}),
2710
1593
  },
2711
1594
  };
2712
1595
  }
2713
- // Cancelled mid-resume (the abort threw out of the resumed stream): settle the row to the cancelled
2714
- // terminal (parity with runInBackground) instead of bubbling a 500 that leaves a zombie `running` row
2715
- // for the reaper. The operator's /decide gets the run's final state, not an opaque error. (A preempt mid-
2716
- // resume is NOT an abort — it durably suspends and surfaces above as status:"suspended", not here.)
2717
1596
  if (cancelCtrl.signal.aborted) {
2718
1597
  if (taskId && deps.runStore) {
2719
1598
  const c = { taskId, sessionId, status: "failed", errorCode: "cancelled", errorMessage: "cancelled by user", stats: { turns: 0, tokens: 0 } };
2720
1599
  await deps.runStore.setTerminal(taskId, "failed", c, c.errorMessage ?? null).catch(() => undefined);
2721
1600
  }
2722
- settleFleet("failed"); // MF-Fleet (#7): a cancel mid-resume settles the row terminal → removed from the fleet
1601
+ settleFleet("failed");
2723
1602
  return { status: 200, body: { taskId, sessionId, status: "failed", errorCode: "cancelled" } };
2724
1603
  }
2725
- // C: a plain Error (not CheckpointError, not a cancel) is about to propagate before the post-catch
2726
- // `settleFleet(result.status)` can run → settle the re-published row to "failed" first so it doesn't leak
2727
- // as a stale "running" (no-op if a CheckpointError/cancel branch above already settled it).
2728
- // 🔴 codex R1-F2(2026-08-14):行也要落终局,不只 fleet 帧——本腿在 markResuming/createRun 赢下行后
2729
- // 普通异常一路 throw 出去,行曾一直留 running+claim 在手直到 reapStale(期间 session 被锁死,最终被
2730
- // 误记 instance-lost)。cancel 分支(上方)早为同一理由落行,本臂是漏掉的孪生。claimedRow 守卫=只有
2731
- // 抢到行的腿才有资格驱终局(与 CheckpointError 臂同一条纪律)。
2732
1604
  if (claimedRow && taskId && deps.runStore) {
2733
1605
  await deps.runStore.setTerminal(taskId, "failed", null, e instanceof Error ? e.message : String(e)).catch(() => undefined);
2734
1606
  }
@@ -2736,78 +1608,45 @@ export function createHttpServer(rawDeps) {
2736
1608
  throw e;
2737
1609
  }
2738
1610
  finally {
2739
- unregisterEngineNotice?.(); // #310:腿结束即注销 wire 口
1611
+ unregisterEngineNotice?.();
2740
1612
  if (taskId) {
2741
- // [1.207 codex H3] identity-guarded like steerableRuns below — a retriable-CheckpointError re-park
2742
- // (setSuspended) opens a window where a fast sibling resume markResuming's + registers its OWN
2743
- // controllers under the same taskId before this finally runs; an unconditional delete killed the
2744
- // fresh handles (cancel/preempt would 409 "another replica" until re-registration).
2745
1613
  if (inflightRuns.get(taskId) === cancelCtrl)
2746
1614
  inflightRuns.delete(taskId);
2747
1615
  if (preemptableRuns.get(taskId) === preemptCtrl)
2748
1616
  preemptableRuns.delete(taskId);
2749
- // Identity-guarded (1.78 review LOW): only delete the handle THIS leg registered (and only if it is
2750
- // still ours) — a faster sibling resume may have re-registered the same taskId already; never registered
2751
- // ⇒ nothing of ours to delete.
2752
1617
  if (resumeStreamRef !== undefined && steerableRuns.get(taskId) === resumeStreamRef)
2753
1618
  steerableRuns.delete(taskId);
2754
1619
  for (const evict of subagentHandleEvictions)
2755
- evict(); // design/122 ③: leg-end handle eviction (identity-guarded)
2756
- deps.modelUsage?.clear(taskId); // E8: parity with runInBackground's finally — unregister + drop any un-drained accumulation (no leak / no cross-resume contamination)
2757
- deps.promptManifests?.clear(taskId, resumeManifestLease); // [998]②: lease-guarded (a fast sibling's fresh registration survives our teardown)
1620
+ evict();
1621
+ deps.modelUsage?.clear(taskId);
1622
+ deps.promptManifests?.clear(taskId, resumeManifestLease);
2758
1623
  }
2759
1624
  if (resumeHeartbeat)
2760
1625
  clearInterval(resumeHeartbeat);
2761
- resumeLegLive = false; // notifications from here on take the durable-inbox path
1626
+ resumeLegLive = false;
2762
1627
  }
2763
- // A session-CAS conflict can surface as a failed result on resume too — evict the stale warm-cache entry so
2764
- // the next task on this session re-wakes from TiDB instead of re-conflicting (parity with runInBackground /
2765
- // sync POST / cascade; the only terminal-result path that was missing this — council).
2766
1628
  evictIfConflict(deps.runner, sessionId, result);
2767
- // [1245] codex-3: the two REOPEN-class resume failures (`resume.env_failed` / `resume.tool_unavailable` —
2768
- // core dist-read: runtask reopens the checkpoint via store.reopen BEFORE returning this failed result; a
2769
- // reopen that itself fails surfaces as `checkpoint.reopen_failed`, NOT these codes) leave the checkpoint
2770
- // PENDING. Driving the row terminal here would release task_active and orphan the still-decidable park
2771
- // (the exact class the CheckpointError `retriable` branch above re-parks) — e.g. Workflow entitlement
2772
- // revoked between suspend and approve. Re-park `suspended` so the operator's retry can claim it again.
2773
- const checkpointReopened = reopenConfirmedPark; // codex-5: store-confirmed inside the resume paths — never inferred from the code alone
2774
- // Drive the parked run row to its new state (terminal, or re-suspended on a further gate). Strip the token
2775
- // from the persisted result — structural fence (no-op today; GET /v1/runs/:id reads this row back).
1629
+ const checkpointReopened = reopenConfirmedPark;
2776
1630
  if (taskId && deps.runStore) {
2777
1631
  if (result.status === "suspended" || checkpointReopened)
2778
1632
  await deps.runStore.setSuspended(taskId);
2779
1633
  else if (result.status === "needs_review")
2780
- await deps.runStore.setNeedsReview(taskId); // D-B: a resumed leg re-gated to a review pause — re-park, keep the lock
1634
+ await deps.runStore.setNeedsReview(taskId);
2781
1635
  else
2782
1636
  await deps.runStore.setTerminal(taskId, result.status, stripCheckpointToken(result), result.errorMessage ?? null);
2783
1637
  }
2784
- // MF-Fleet (#7): settle the fleet row in lock-step with the run row — suspended/needs_review KEEP the row (it stays
2785
- // "waiting"/"awaiting approval"); a terminal status REMOVES it (onTerminal does this mapping). Mirrors the sync leg.
2786
- settleFleet(checkpointReopened ? "suspended" : result.status, fleetRunResiduals(result)); // [1839]①/(a):终态残局键(parked 时 publisher 不上帧)
2787
- // Surface the failure code/message to the deciding operator (never the token): a resumed leg that fails
2788
- // otherwise returns an opaque {status:"failed"} — undiagnosable without a run row (leader sub-workers).
1638
+ settleFleet(checkpointReopened ? "suspended" : result.status, fleetRunResiduals(result));
2789
1639
  return {
2790
1640
  status: 200,
2791
1641
  body: {
2792
1642
  taskId, sessionId, status: result.status,
2793
- // [2400] HITL-12:世代探测位——本 server 强制决策-动作绑定(无绑定 decide 已在上方 400 拒),
2794
- // 客户端凭它机器确认「我裁的就是我看到的那个动作」,不再有静默回落窗口。
2795
1643
  bindingEnforced: true,
2796
1644
  ...(result.errorCode ? { errorCode: result.errorCode } : {}),
2797
1645
  ...(result.errorMessage ? { errorMessage: redactSecrets(result.errorMessage) } : {}),
2798
- ...(checkpointReopened ? { retriable: true } : {}), // the park is still pending — re-fetch + re-decide
1646
+ ...(checkpointReopened ? { retriable: true } : {}),
2799
1647
  },
2800
1648
  };
2801
1649
  }
2802
- /**
2803
- * design/80 seam #2 (scheduler resume): resume a task that durably SUSPENDED on a `resource_limit` gate — a
2804
- * scheduler PREEMPT (reason "preempt") or a budget/turns/walltime slice boundary (design/74). Distinct from the
2805
- * operator `/decide` path (`resumeCheckpoint`, `policy_ask`): there is NO human decision, NO binding, NO answer
2806
- * — just `{gate:"resource_limit", decision:"continue"}` (core computes the next slice's allowance from the
2807
- * ledger). 🔴 GATE-KIND GUARD: it REFUSES to resume a `policy_ask`/human gate via this path (that would bypass
2808
- * the human approval) — a non-resource gate returns 409 and must go through `/decide`. Owner-scoped by the
2809
- * caller; `scope` is the checkpoint's own (multi-tenant). Returns the HTTP {status, body}.
2810
- */
2811
1650
  async function resumePreempted(sessionId, req) {
2812
1651
  const cs = deps.checkpointStore;
2813
1652
  const token = await cs.findPendingTokenBySession(sessionId);
@@ -2815,18 +1654,13 @@ export function createHttpServer(rawDeps) {
2815
1654
  return { status: 404, body: { error: "no resumable suspension for this task (already running, resumed, or expired)", errorCode: "not_found.suspension" } };
2816
1655
  let cp;
2817
1656
  try {
2818
- cp = await cs.get(token); // throws if the checkpoint format is newer than this build (version guard)
1657
+ cp = await cs.get(token);
2819
1658
  }
2820
1659
  catch (e) {
2821
1660
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
2822
1661
  }
2823
1662
  if (!cp)
2824
1663
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
2825
- // 🔴 GATE-KIND GUARD (security): this "continue" path must ONLY advance a resource_limit suspend. A
2826
- // policy_ask / human / irreversible_ask gate requires a HUMAN decision via POST /v1/approvals/:id/decide —
2827
- // resuming it here with decision:"continue" would BYPASS the approval. Reject it distinctly (core's resume
2828
- // gate-match is the fail-closed backstop, but we reject pre-CAS for a clear error + to never consume the
2829
- // checkpoint). dry_run_review / plan_review likewise are not "continue"-resumable here.
2830
1664
  const gateKind = cp.gate?.kind;
2831
1665
  if (gateKind !== "resource_limit") {
2832
1666
  return { status: 409, body: { error: `task is suspended on a '${gateKind ?? "unknown"}' gate, not a resumable resource/preempt suspension — an approval gate must be decided via POST /v1/approvals/:id/decide`, errorCode: "gate_not_resumable" } };
@@ -2834,33 +1668,23 @@ export function createHttpServer(rawDeps) {
2834
1668
  const ctx = await cs.getCtx(sessionId).catch(() => null);
2835
1669
  if (!ctx)
2836
1670
  return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
2837
- // Rebuild taskConfig via resolveSpec with auth reconstructed from the checkpoint scope (mirrors resumeCheckpoint).
2838
1671
  const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
2839
- // E18: STRIP resumeAt before re-resolving on a durable resume — the leaf is already set by the checkpoint, and
2840
- // resumeAt + a durable resume is rejected by core (resume_at.conflicts_resume); also avoids re-resolving a stale
2841
- // anchor (which could 404 if the session was purged). A resume-at run that suspended stored its body with resumeAt.
2842
1672
  const spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
2843
1673
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
2844
- // A resource-suspended task that was submitted with verify:true is re-gated on completion (parity with the
2845
- // HITL path — the flag rides the persisted ctx.body).
2846
1674
  const verifyRounds = verifyRoundsFromBody(ctx.body);
2847
1675
  return driveResumeIntoRunLog({ runner: runnerFor(spec), token, sessionId: cp.sessionId, principal: auth.principal, fleetScope: resumeFleetScope(req, auth), taskConfig, resumeObjective, outcome: { gate: "resource_limit", decision: "continue" }, verifyRounds });
2848
1676
  }
2849
- /** design/144 §3:wake 一个 task_done 纯 park——非门决策,解除 park 消费消息续跑。owner 门:非 operator
2850
- * 只能 wake 自己 scope 的 park(404 无 oracle);core 端 gate_pending/nothing_to_deliver 双拒兜底。 */
2851
1677
  async function resumeWake(sessionId, message, caller, req) {
2852
1678
  const cs = deps.checkpointStore;
2853
1679
  const token = await cs.findPendingTokenBySession(sessionId);
2854
1680
  if (!token)
2855
1681
  return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
2856
- // owner 门先于 get(复审 finding:get 对损坏/新版行会抛,若 gate 在后,非 owner 能用 409 错误详情
2857
- // 区分「存在但坏」vs「不存在」= 存在性 oracle)。scope 走 peekPendingScope(轻读,不碰行体)。
2858
1682
  const operator = explicitOperatorOk(caller, deps.config.operatorPrincipals);
2859
1683
  if (!operator) {
2860
1684
  const scope = await cs.peekPendingScope(sessionId).catch(() => undefined);
2861
1685
  const decodedScope = scope != null ? decodeCheckpointScope(scope) : undefined;
2862
1686
  if (decodedScope !== undefined && decodedScope !== caller) {
2863
- return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } }; // 无 oracle
1687
+ return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
2864
1688
  }
2865
1689
  }
2866
1690
  let cp;
@@ -2872,18 +1696,14 @@ export function createHttpServer(rawDeps) {
2872
1696
  }
2873
1697
  if (!cp)
2874
1698
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
2875
- // 纵深(peek 缺席面兜底)。比较留在 scope 空间(常量比较,不 decode):行体可能来自不做形状校验的
2876
- // JSON 读取,scope 越契约为 nullish 时 decode 形会把「畸形行」折成「公开」= 放行;旧码对该支是拒绝
2877
- // 方向,常量形逐字节保持它(codex 码审 finding,#97 α 车)。
2878
1699
  if (!operator && cp.scope !== CHECKPOINT_PUBLIC_SCOPE && cp.scope !== caller) {
2879
1700
  return { status: 404, body: { error: "no parked checkpoint for this session (nothing to wake)", errorCode: "not_found.parked_checkpoint" } };
2880
1701
  }
2881
- // 预检(core 兜底同码):非纯 park = 有未决门,wake 永不绕审批。
2882
1702
  const gateKind = cp.gate?.kind;
2883
1703
  if (gateKind !== "task_done") {
2884
1704
  return { status: 409, body: { error: `cannot wake: this checkpoint awaits a pending gate decision (gate "${gateKind ?? "unknown"}") — resolve it through its own decide entry`, errorCode: "wake.gate_pending" } };
2885
1705
  }
2886
- const trusted = operator; // steer 同款:operator 消息可携 <system-reminder> 权威
1706
+ const trusted = operator;
2887
1707
  if (message !== undefined) {
2888
1708
  try {
2889
1709
  validatePendingSteer({ text: message, trusted });
@@ -2910,29 +1730,12 @@ export function createHttpServer(rawDeps) {
2910
1730
  taskConfig,
2911
1731
  resumeObjective,
2912
1732
  outcome: { gate: "wake", ...(message !== undefined ? { message: { text: message, trusted } } : {}) },
2913
- verifyRounds: undefined, // wake 续跑不重开 verify 门(原任务已终局;新工作要 verify 走新 run)
2914
- mintFreshRun: { source: "wake" }, // [3829]:终局纯 park 的 claim 已删,声明铸新行(注释见 DriveResumeArgs)
1733
+ verifyRounds: undefined,
1734
+ mintFreshRun: { source: "wake" },
2915
1735
  });
2916
1736
  }
2917
- /**
2918
- * design/80 D-B: resolve a PRE-ACTION `plan_review` gate (3-state approve | edit | reject) and drive the resumed
2919
- * leg into the durable run log via the SHARED machinery. DISTINCT from `resumeCheckpoint` (a `policy_ask` tool
2920
- * approval, allow/deny + binding) and `resumePreempted` (`resource_limit` continue). The `edit` decision carries
2921
- * the operator's REVISED plan (core threads `editedPlan` into the resumed run). 🔴 GATE-KIND GUARD (security):
2922
- * refuses any non-`plan_review` gate — an approve/edit/reject must NEVER resolve a tool-approval / resource_limit
2923
- * suspend (that would bypass the correct /decide allow-deny + action binding). Rejects PRE-CAS so a wrong-gate
2924
- * call never consumes the checkpoint (core's resume gate-match is the fail-closed backstop).
2925
- */
2926
- async function resumePlanReview(sessionId, decision, editedPlan, reason, req,
2927
- /** [4277]/[4288] 件5:判别日志的两个附加位。调用方(plan_review 端点)手上有 taskId 与 gatedPrincipal,
2928
- * 本函数只有 sessionId —— 穿进来是为了让 `decide_404` 的**四个 reason 共用同一套字段**(联调 grep 一条
2929
- * msg 就拿到同一个 id 键与同一个 principalPresent 口径,不必按 reason 分两种形)。
2930
- * 缺席 ⇒ id 退回 sessionId 并如实换键名(不假装那是 taskId)。 */
2931
- log) {
1737
+ async function resumePlanReview(sessionId, decision, editedPlan, reason, req, log) {
2932
1738
  const cs = deps.checkpointStore;
2933
- // [4268]/[4274]/[4277]/[4288] 件5:404 成因判别位——**只进服务端日志,不上 wire**(响应体一字不改,
2934
- // 反枚举不破)。词表四值全在本函数与 approvals-assistant.ts 两处铸,合起来覆盖 plan_review 一条决裁
2935
- // 路径的全部 404 出口。`principalPresent` 只记**有无**:身份串一个都不许进日志(日志读者面比 wire 宽)。
2936
1739
  const log404 = (r) => deps.logger?.info?.("decide_404", {
2937
1740
  reason: r,
2938
1741
  ...(log?.taskId !== undefined ? { taskId: log.taskId } : { sessionId }),
@@ -2945,7 +1748,7 @@ export function createHttpServer(rawDeps) {
2945
1748
  }
2946
1749
  let cp;
2947
1750
  try {
2948
- cp = await cs.get(token); // throws if the checkpoint format is newer than this build (version guard)
1751
+ cp = await cs.get(token);
2949
1752
  }
2950
1753
  catch (e) {
2951
1754
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
@@ -2954,9 +1757,6 @@ export function createHttpServer(rawDeps) {
2954
1757
  log404("checkpoint-missing");
2955
1758
  return { status: 404, body: { error: "checkpoint not found", errorCode: "not_found.checkpoint" } };
2956
1759
  }
2957
- // 🔴 GATE-KIND GUARD: a plan_review decision must ONLY resolve a plan_review gate. A human/irreversible_ask
2958
- // (tool approval) or resource_limit/needs_review gate routed here would let approve/edit/reject bypass the
2959
- // correct wire (a tool approval needs /decide's allow/deny + action binding; resource_limit needs /resume).
2960
1760
  const gateKind = cp.gate?.kind;
2961
1761
  if (gateKind !== "plan_review") {
2962
1762
  return {
@@ -2970,15 +1770,9 @@ export function createHttpServer(rawDeps) {
2970
1770
  const ctx = await cs.getCtx(sessionId).catch(() => null);
2971
1771
  if (!ctx)
2972
1772
  return { status: 409, body: { error: "resume context missing — cannot rebuild task config", errorCode: "conflict.resume_context_unavailable" } };
2973
- // Rebuild taskConfig via resolveSpec with auth reconstructed from the checkpoint scope (mirrors resumeCheckpoint).
2974
1773
  const auth = { sessionId: cp.sessionId, principal: decodeCheckpointScope(cp.scope), memoryScope: ctx.memoryScope };
2975
- // E18: STRIP resumeAt before re-resolving on a durable resume — the leaf is already set by the checkpoint, and
2976
- // resumeAt + a durable resume is rejected by core (resume_at.conflicts_resume); also avoids re-resolving a stale
2977
- // anchor (which could 404 if the session was purged). A resume-at run that suspended stored its body with resumeAt.
2978
1774
  const spec = await deps.resolveSpec({ ...ctx.body, resumeAt: undefined }, req, auth);
2979
1775
  const { objective: resumeObjective, sessionId: _sessionId, ...taskConfig } = spec;
2980
- // A plan_review-suspended task submitted with verify:true is re-gated on completion (parity with the other
2981
- // resume paths — the flag rides the persisted ctx.body).
2982
1776
  const verifyRounds = verifyRoundsFromBody(ctx.body);
2983
1777
  const outcome = {
2984
1778
  gate: "plan_review",
@@ -2994,40 +1788,26 @@ export function createHttpServer(rawDeps) {
2994
1788
  deps.metrics?.inc("task_tokens_total", {}, result.stats.tokens);
2995
1789
  if (result.stats.cacheHitRate !== undefined)
2996
1790
  deps.metrics?.observe("task_cache_hit_rate", result.stats.cacheHitRate);
2997
- // ⑤ budget/limit cutoffs:count failures by their dotted code — core 5.8.0 起统一 `limits.` 前缀
2998
- // (limits.max_{tokens,cost,turns,walltime}_exceeded;旧 budget.*/limit.* 双前缀同拍退役),
2999
- // operator 按此对租户撞顶告警。metric 名保留 budget_exceeded_total(时序连续性;code label 自述新码)。
3000
1791
  const code = result.errorCode;
3001
1792
  if (code && code.startsWith("limits.")) {
3002
1793
  deps.metrics?.inc("budget_exceeded_total", { code });
3003
1794
  }
3004
- // Developer-mode verification gate (1.44): count the final verdict when this task was verified.
3005
1795
  const verdict = result.verification?.verdict;
3006
1796
  if (verdict)
3007
1797
  deps.metrics?.inc("verifications_total", { verdict });
3008
- // Cascade (1.45): count the outcome (passed / exhausted) when this task ran the ladder.
3009
1798
  const cascadeOutcome = result.cascadeOutcome;
3010
1799
  if (cascadeOutcome)
3011
1800
  deps.metrics?.inc("cascade_total", { outcome: cascadeOutcome });
3012
- // Degenerate-repetition salvage classifier (1.59): a/b/unknown — see degenerate-instrument.ts.
3013
1801
  deps.instrumentDegenerate?.(result);
3014
1802
  }
3015
- /** The terminal-result side-effect trio (metrics, plan-cache recurrence probe, warm-cache eviction) in ONE
3016
- * place so a terminal path can't silently omit one (council DESIGN-6 — the omissions caused tracking gaps). */
3017
1803
  function finalizeTaskResult(result, principal, objective, sessionId) {
3018
1804
  recordTaskResult(result);
3019
1805
  deps.planCacheProbe?.record(principal, objective, result.status === "completed");
3020
1806
  evictIfConflict(deps.runner, sessionId, result);
3021
1807
  }
3022
- /** Per-principal cumulative cost quota: 429 + retry-after when the principal is over for the window.
3023
- * No principal → not gated (the rate limiter + REQUIRE_PRINCIPAL still apply). */
3024
1808
  function quotaExceeded(req, res) {
3025
1809
  if (!deps.costQuota)
3026
1810
  return false;
3027
- // 🔴 F-fix: the VERIFIED principal (gatedPrincipal), never the spoofable header. Spend ACCRUES to the verified
3028
- // principal (budget.ts `costQuota.add(currentPrincipal())` = auth.principal), so checking a header-keyed bucket
3029
- // while the verified bucket fills would let a direct-door caller dodge its quota by rotating x-agent-principal.
3030
- // The sibling rateLimited keys on the socket on a direct door for the same reason — this was its overlooked twin.
3031
1811
  const principal = gatedPrincipal(req, deps.config);
3032
1812
  if (!principal)
3033
1813
  return false;
@@ -3039,12 +1819,6 @@ export function createHttpServer(rawDeps) {
3039
1819
  sendError(res, 429, "limit.cost_quota_exceeded", "cost quota exceeded", { usedMicroUsd: d.usedMicroUsd, limitMicroUsd: d.limitMicroUsd, retryAfterSec: d.retryAfterSec });
3040
1820
  return true;
3041
1821
  }
3042
- /** lease admission(D4 AP)— the quotaExceeded sibling for lease-enforced principals: first
3043
- * touch negotiates with center (single-flight; 409 not-lease-enforced → negative-cached pass-through to
3044
- * the local CostQuota path), a live lease admits locally, center-judged exhaustion denies 429+Retry-After
3045
- * (same shape as the cost-quota deny), and an unreachable center FAILS OPEN (AP posture — reconciliation
3046
- * is the backstop). Keys on the VERIFIED principal only (gatedPrincipal — the quotaExceeded F-fix twin);
3047
- * no principal (single-user turnkey) → no lease surface. */
3048
1822
  async function leaseDenied(req, res) {
3049
1823
  if (!deps.fleetLease)
3050
1824
  return false;
@@ -3056,17 +1830,9 @@ export function createHttpServer(rawDeps) {
3056
1830
  return false;
3057
1831
  deps.metrics?.inc("fleet_lease_rejected_total");
3058
1832
  res.setHeader("retry-after", String(adm.retryAfterSec));
3059
- // E4 定稿:429 带 typed code + center 结构化字段 verbatim 透传(windowType/remaining/resetAt/
3060
- // isEstimate/pool)——人话文案归壳/web(契约如此),error 串保留旧消费者兼容。
3061
1833
  sendError(res, 429, "quota_exhausted", "budget exhausted (quota lease)", { retryAfterSec: adm.retryAfterSec, ...(adm.detail ?? {}) });
3062
1834
  return true;
3063
1835
  }
3064
- /** design/166-T1 — 治理窗 pre-admission(quotaExceeded/leaseDenied 的兄弟门):key 公式与 core 单源
3065
- * (`principal || GLOBAL_USAGE_KEY`,GLOBAL_USAGE_KEY 引 core 公共导出防漂移;principal=verified,
3066
- * spoofable header 不进键)。窗耗尽 → 429 + retryAfterSec(族形一致,秒上取整)+ Retry-After 头。
3067
- * 读窗失败(DB 瞬断/坏 record)→ warn + fail-open:enforcement 真源在 core 进场检查(同店同键,
3068
- * 会响亮拒),此门只是异步车道的 429 快速座——门故障不该把提交面整个拖死。admission-pass 与
3069
- * core entry-拒之间存在竞态窗:该形落终态 run 记录(errorCode usage.window_exhausted),契约文档在案。 */
3070
1836
  async function usageWindowDenied(req, res) {
3071
1837
  const store = deps.usageWindowStore;
3072
1838
  const windows = deps.config.usageWindows;
@@ -3090,13 +1856,9 @@ export function createHttpServer(rawDeps) {
3090
1856
  sendError(res, 429, "usage.window_exhausted", "usage window exhausted (deployment token governance)", { retryAfterSec });
3091
1857
  return true;
3092
1858
  }
3093
- /** Apply the per-principal rate limit; responds 429 and returns true when the caller is over. */
3094
1859
  function rateLimited(req, res) {
3095
1860
  if (!deps.rateLimiter)
3096
1861
  return false;
3097
- // 🔴 D-G (DeepSeek council Bug#2): on a direct-mode worker x-agent-principal is UNTRUSTED, so keying the limiter
3098
- // on it lets a client rotate the header to evade its own bucket OR set a victim's principal to poison theirs
3099
- // (DoS). Key on the socket address instead (the verified principal isn't known until post-body JWT verify).
3100
1862
  const key = (deps.config.directDoorActive ? undefined : principalFrom(req, deps.config)) ?? req.socket.remoteAddress ?? "anon";
3101
1863
  const d = deps.rateLimiter.check(key);
3102
1864
  if (d.allowed)
@@ -3106,14 +1868,6 @@ export function createHttpServer(rawDeps) {
3106
1868
  sendError(res, 429, "limit.rate_exceeded", "rate limit exceeded", { retryAfterSec: d.retryAfterSec });
3107
1869
  return true;
3108
1870
  }
3109
- /**
3110
- * §0.5 session-scoped owner scope — the SAME model as GET /v1/sessions, reused by the fork/delete routes so
3111
- * all three agree on "who may touch this session". A system/traceToken caller is FLEET-WIDE (ops, any session);
3112
- * a principal caller is PINNED to `gateOwner` (its own). Identity is `gatedPrincipal` (direct-door safe), never
3113
- * the spoofable header. Caller checks: `!fleetWide && gateOwner===null` ⇒ 401; otherwise compare the session's
3114
- * stored owner to `gateOwner` (principal path) or allow (fleetWide).
3115
- */
3116
- /** Decode a URL path segment, returning null on a malformed %-sequence (a client error → 400, never a 500). */
3117
1871
  function safeDecode(seg) {
3118
1872
  try {
3119
1873
  return decodeURIComponent(seg);
@@ -3122,11 +1876,6 @@ export function createHttpServer(rawDeps) {
3122
1876
  return null;
3123
1877
  }
3124
1878
  }
3125
- // 🔒 Single source for the fleet-wide (all-tenant) gate. dev-open (no service/trace token ⇒ fleet-wide) is honored
3126
- // ONLY on a single-user deployment. On MULTI-TENANT (requirePrincipal=true), the ABSENCE of a token must NOT open the
3127
- // fleet — that would leak every tenant's sessions/runs/fleet rows to ANY caller (HIGH tenant-isolation gap, systematic
3128
- // audit 2026-06-30). Multi-tenant without a token ⇒ NOT fleet-wide ⇒ owner-gated by principal (and main.ts boot-guards
3129
- // it to fail FAST). A system authToken or the read-only traceToken still grant fleet-wide on any deployment.
3130
1879
  function isFleetWide(req) {
3131
1880
  const { authToken: at, traceToken: tt } = deps.config;
3132
1881
  const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
@@ -3137,11 +1886,6 @@ export function createHttpServer(rawDeps) {
3137
1886
  const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
3138
1887
  return { fleetWide, gateOwner };
3139
1888
  }
3140
- /** 🔴 WRITE-grade fleet-wide (2026-07-13): the READ-ONLY traceToken must never authorize a
3141
- * cross-tenant WRITE (delete/fork/sync import) — isFleetWide deliberately grants it fleet-wide for the trace/
3142
- * list READ surfaces, but consuming that same bit on destructive routes turned a read credential into write
3143
- * authority. Write routes use THIS gate: dev-open (single-user, no tokens) and the SYSTEM authToken keep their
3144
- * authority; a traceToken-only caller falls through to the principal owner gate (or 401). */
3145
1889
  function sessionOwnerScopeForWrite(req) {
3146
1890
  const { authToken: at, traceToken: tt } = deps.config;
3147
1891
  const anyAt = Boolean(at) || Object.keys(deps.config.authTokens ?? {}).length > 0;
@@ -3149,41 +1893,19 @@ export function createHttpServer(rawDeps) {
3149
1893
  const gateOwner = fleetWide ? null : (gatedPrincipal(req, deps.config) || null);
3150
1894
  return { fleetWide, gateOwner };
3151
1895
  }
3152
- /** Reject when the caller's principal does not own the run (null owner = open, dev). */
3153
1896
  function runOwnerOk(req, res, owner) {
3154
- // 🔴 F-fix: the VERIFIED principal (gatedPrincipal), never the spoofable header — `owner` was persisted from the
3155
- // verified principal at create, so comparing it to a spoofable header would let a direct-door caller read/cancel a
3156
- // victim's run (and its event stream) by sending x-agent-principal:<victim> with NO JWT. Same model as
3157
- // sessionOwnerScope. (BFF/gated: gatedPrincipal === principalFrom, byte-identical.)
3158
1897
  const principal = gatedPrincipal(req, deps.config);
3159
1898
  if (deps.config.requirePrincipal && !principal) {
3160
1899
  sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
3161
1900
  return false;
3162
1901
  }
3163
- // [3273] C8:窄互认的读面半场——[3262] 只修了提交/续发面(createAuthorizer),head/audit/events
3164
- // 三读面走本函数,7.8.0 未同窗 ⇒ cli 停发哨兵头后存量会话「能续跑但看不见」,且 404 与「压根
3165
- // 不存在」黑盒同形(test [3273] 实测)。同 security.ts 三条件窄臂,语义/墓碑注同源(存量随会话
3166
- // 生命周期消亡后可整删);带具名 principal 的请求对哨兵行仍走下方 404 无 oracle 形。
3167
1902
  const legacyShellOwner = !deps.config.requirePrincipal && owner === "anon:shell-live" && (principal === undefined || principal === null);
3168
1903
  if (!legacyShellOwner && owner !== null && principal !== owner) {
3169
- sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE); // #262:与 unknown 臂同串——「don't reveal existence」的意图此前被长短文差打穿
1904
+ sendError(res, 404, "not_found.run", RUN_NOT_FOUND_MESSAGE);
3170
1905
  return false;
3171
1906
  }
3172
1907
  return true;
3173
1908
  }
3174
- /** [1499] G — session-scoping ACCEPT phase on the run faces ([1498]⑥ soft rollout, 第一拍). principal answers
3175
- * "whose data" (authz axis); session answers "which conversation" (content axis, [1493]). These faces deliver
3176
- * conversation content (poll/events) or act on a conversation (cancel/steer/compact/detach, subagent
3177
- * steer/resume) but were principal-gated only. Enforcement (absent ⇒ 404, subagentOutput-style) needs every
3178
- * caller migrated first, so THIS phase 404s ONLY a PRESENT-but-MISMATCHED `?session=`: an honest caller
3179
- * asserting its own session never mismatches on its own run — a mismatch IS the accidental cross-session
3180
- * touch. Absent ⇒ allowed + warn-once per face (migration observability for the enforcement train). NB the
3181
- * operator branch below is only REACHABLE where the preceding owner gate admits the caller: on faces whose
3182
- * owner gate carries its own trusted arm (steer/compact/detach/subagent verbs) it is fully live; on the
3183
- * runOwnerOk faces (poll/events/cancel — no operator arm, [1498]⑥ ledger) a cross-owner operator 404s at
3184
- * the owner gate first, so here it covers only owner-null runs or an operator who IS the owner. Widening
3185
- * those owner gates is a deliberate [1498]⑥ decision, not this phase's. A session-unbound run has nothing
3186
- * to assert against. */
3187
1909
  const sessionAssertAbsentWarned = new Set();
3188
1910
  function runSessionAcceptOk(req, res, run, face, notFoundError = RUN_NOT_FOUND_MESSAGE) {
3189
1911
  if (!run.sessionId)
@@ -3200,7 +1922,7 @@ export function createHttpServer(rawDeps) {
3200
1922
  return true;
3201
1923
  if (explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals))
3202
1924
  return true;
3203
- sendError(res, 404, "not_found.run", notFoundError); // byte-identical to the face's own owner-gate wording (no distinguishable arm)
1925
+ sendError(res, 404, "not_found.run", notFoundError);
3204
1926
  return false;
3205
1927
  }
3206
1928
  async function readJson(req) {
@@ -3218,13 +1940,9 @@ export function createHttpServer(rawDeps) {
3218
1940
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
3219
1941
  }
3220
1942
  catch {
3221
- // F1 (center K3S report): a malformed body is a client error → 400, not an uncaught 500.
3222
1943
  throw new HttpError(400, "invalid JSON body");
3223
1944
  }
3224
1945
  }
3225
- /** 2c session-sync: read a RAW request body (binary blob bytes) with a running per-request size cap — the bytes are
3226
- * NEVER buffered unboundedly (the §5 "reject, not OOM" guard). Throws HttpError(413) the instant `max` is exceeded
3227
- * (mid-stream, before the whole body is in memory). Returns the concatenated Buffer (≤ max). */
3228
1946
  async function readRawBody(req, max) {
3229
1947
  const chunks = [];
3230
1948
  let size = 0;
@@ -3236,15 +1954,6 @@ export function createHttpServer(rawDeps) {
3236
1954
  }
3237
1955
  return Buffer.concat(chunks);
3238
1956
  }
3239
- /**
3240
- * design/80 D-D (SLA-timer deny-sweep): resolve-DENY every pending human/irreversible_ask approval past its
3241
- * deadline (the per-approval SLA) — re-resume each with decision=deny so the model CONTINUES with the denial
3242
- * (graceful; vs the abort the reaper's expire() gives resource_limit/needs_review). Reuses resumeCheckpoint,
3243
- * so the markResuming CAS makes it idempotent across replicas (one replica wins each resume) and the parked
3244
- * run row is driven correctly. A per-call failure (e.g. a redeployed scenario) is swallowed → the row stays
3245
- * pending and is retried next tick, with the terminal_at_ms backstop as the eventual abort if the deny never
3246
- * succeeds. Bounded per tick by the store query's LIMIT. Wired into main.ts's reaper.
3247
- */
3248
1957
  const denySweepClockGuard = createWallClockJumpGuard({
3249
1958
  slaMs: deps.config.approvalTimeoutSec > 0 ? deps.config.approvalTimeoutSec * 1000 : undefined,
3250
1959
  onJump: ({ jumpMs, graceMs }) => deps.logger?.warn?.("deny_sweep_wall_clock_jump", {
@@ -3255,8 +1964,6 @@ export function createHttpServer(rawDeps) {
3255
1964
  : "the wall clock stepped BACKWARD relative to the monotonic clock; the deny-sweep needs no grace for this direction (a receding deadline only DELAYS a deny) but the step itself is an operational fact worth recording.",
3256
1965
  }),
3257
1966
  onSuppressed: ({ remainingMs }) => deps.logger?.info?.("deny_sweep_clock_grace", { remainingMs }),
3258
- // codex R2 [high] 五:连续跳变撞上累计上限 ⇒ 守卫**响亮**恢复判定。这一行是运维必须看见的:
3259
- // 它说的是「本机墙钟在持续抖,我不再为它让路了,接下来的 deny 判定基于一个不稳的钟」。
3260
1967
  onGraceCapped: ({ totalGraceMs }) => deps.logger?.warn?.("deny_sweep_clock_grace_capped", {
3261
1968
  totalGraceMs,
3262
1969
  note: "the wall clock kept stepping forward; the approval SLA deny-sweep has stood down for the maximum cumulative grace and now RESUMES on a demonstrably unstable clock — a fail-closed leg that never runs is worse than one running on a bad clock. Fix NTP on this host.",
@@ -3266,18 +1973,9 @@ export function createHttpServer(rawDeps) {
3266
1973
  const cs = deps.checkpointStore;
3267
1974
  if (!cs)
3268
1975
  return;
3269
- // [4232]①/[4235]:**判定**钟的单调佐证。`now` 是墙钟(生产由 reaper 传 `Date.now()`),而
3270
- // `checkpoint.deadline` 是 core 按铸卡当时的墙钟算的绝对时刻 —— 墙钟凭空前跳 ⇒ 判定钟凭空多走同样的量,
3271
- // 人还没走开窗就没了(真机实测:SLA 45s,真实等 14s 即被自动拒)。守卫按单调钟宽限整拍让开;
3272
- // **记账钟一律不动**(下面 resumeCheckpoint 的审计/时间戳仍是真墙钟,core 的 totalWaitMs 账面同样不碰)。
3273
1976
  if (!denySweepClockGuard.admit(now))
3274
1977
  return;
3275
1978
  const expired = await cs.listExpiredApprovalGates(now).catch(() => []);
3276
- // [1591] 候裁③ 静默臂:parked 后台子代的过期 cp 不走 sweep 的重活赎回(deadline→terminal_at_ms 窗内
3277
- // 每行每 tick 一条 warn 的噪声源)——其收割属 reaper 的 expire+reconcileParkedAgents 车道。判别在
3278
- // resume 尝试之前做(省掉注定 409 的整条 ctx/resolveSpec 重建尝试;代价=每过期行两次店读,过期集
3279
- // 本就被店查询 LIMIT 界住);命中聚合为单条 info 留痕。判别自身故障 ⇒ 按非 parked 处理(保留 warn,
3280
- // fail-loud 方向)。
3281
1979
  const isParkedOwned = async (sessionId) => {
3282
1980
  if (deps.backgroundAgentStore === undefined)
3283
1981
  return false;
@@ -3296,55 +1994,21 @@ export function createHttpServer(rawDeps) {
3296
1994
  };
3297
1995
  let parkedSkipped = 0;
3298
1996
  for (const { sessionId } of expired) {
3299
- // 对抗评审 2026-07-11: log the swallow — a per-tick failure retried forever (e.g. a policy change now folded
3300
- // into a 409 by resumeCheckpoint, or a redeployed scenario) was fully silent; the row sat pending to its
3301
- // terminal_at_ms with zero operator-visible signal. Behavior unchanged (skip + retry next tick), now diagnosable.
3302
1997
  if (await isParkedOwned(sessionId)) {
3303
1998
  parkedSkipped += 1;
3304
1999
  continue;
3305
2000
  }
3306
- // #204 件6②:出处 = `"timeout"`(窗到期,**没有人**答)。此前这里与人为 /decide 共用一个不带
3307
- // 出处的构造,于是下游把一次自动拒渲染成「有人拒绝了你」。
3308
- // 🕐 **#316 同族登记(本批刻意不改)**:末位 `acceptEarly` 不传 —— 本腿没有在等响应的 HTTP 客户端
3309
- // (它是内部 SLA 车道),受理化对它零收益;而且这条 for 循环**靠 await 串行**来给每 tick 的重活
3310
- // 限流,改成受理即返会让一个 tick 把整批过期卡的模型腿同时点着(下一个问题,不是本批的修)。
3311
2001
  await resumeCheckpoint(sessionId, "deny", "approval SLA expired — auto-denied", "timeout").then((r) => { if (r.status >= 400)
3312
2002
  deps.logger?.warn?.("deny_sweep_resume_blocked", { sessionId, status: r.status, body: r.body }); }, (e) => deps.logger?.warn?.("deny_sweep_resume_failed", { sessionId, err: e instanceof Error ? e.message : String(e) }));
3313
2003
  }
3314
2004
  if (parkedSkipped > 0)
3315
2005
  deps.logger?.info?.("deny_sweep_parked_skipped", { count: parkedSkipped });
3316
2006
  }
3317
- /**
3318
- * #329 —— live 回决腿(`POST /v1/tool-approvals/:id/respond`)对一条**已 park** 的 ask 的赎回席。
3319
- *
3320
- * 语义 = 「壳本来要人手做的那件事,server 替它做完」:PARKED 行上的 gate 三坐标就是
3321
- * `/v1/approvals/:sessionId/decide` 里操作员回显的那三件,于是本席把它们原样喂回**同一条**
3322
- * `resumeCheckpoint` 腿(parked 子代赎回 / 任务级 resume 由那条腿自己 parked-first 分流)。
3323
- * [4872] 锚①「同腿新调用方,零新终局语义」:这里没有第二个终局写点,单赢者仍是那条腿的 CAS
3324
- * (`markResuming` / `claimParkedAgent`),本席只是又一个调用方。
3325
- *
3326
- * 三件刻意的姿势:
3327
- * ① **binding 逐字回显、绝不重算** —— `checkpointToken` 给的是行上的 `gateToken`(= park 那一刻从
3328
- * checkpoint 抄回的那一把):它同时是 D-1 的 stale 判据(gate 已被重铸/已解决 ⇒ 那条腿自己回
3329
- * `approval_stale` 409),所以「决议投给了一张过期的卡」在这条链上是被**拒**的,不是被猜的;
3330
- * ② **sessionId 取 checkpoint 自己的那一个**,不是 ask 行的 —— 委派子代的 park 落在子代会话上,
3331
- * 而 ask 行的 `sessionId` 是**投递上下文**(宿主流)的会话,拿后者去 resume 会打到别人的门上;
3332
- * ③ **acceptEarly=true**(#316):有人正卡在 respond 的 HTTP 上等答复,受理语义是这条腿既有的形。
3333
- */
3334
2007
  async function redeemParkedAsk(req) {
3335
2008
  const cs = deps.checkpointStore;
3336
2009
  if (!cs) {
3337
- // 结构上到不了(协调器的 askStore 与 park 设施同在同不在);诚实拒,绝不假装受理。
3338
2010
  return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
3339
2011
  }
3340
- // 🔴 **计费提交三道门**(codex 对抗复审 R1-[high] 二,验真后修)。`POST /v1/tool-approvals/:id/respond`
3341
- // 刻意**不在** `isBillableSubmitPath` 里,而且必须留在外面:活卡回决是「让在飞的那一轮跑完」,排空期
3342
- // 恰恰要放行(那道门的自注写着 read/cancel/steer/respond 全开)。但**本席不是活卡回决** —— 它开一条新的
3343
- // resume 腿(烧模型),与 `/v1/approvals/:sessionId/decide` 同一件事;那条 url 在名单里、吃这三道门。
3344
- // 于是门贴在**这条臂**上,而不是整条路由上:文案与 errorCode 逐字取 `handle()` 的同三处(同一件事在
3345
- // wire 上只该有一种说法),真值也取同三个 dep(不另立判据)。⚠️ 唯一的形差如实记:那三处是
3346
- // `sendError` + `retry-after` 头,本席只产生**响应体**(路由用 `sendJson` 送),所以没有那个头 ——
3347
- // 体里的码/文案足以让消费端分型,退避节奏由它自己的通用 503 处置管。
3348
2012
  if (!hasServiceAuth(deps.config) && !deps.config.allowUnauthedWrites) {
3349
2013
  return { status: 503, body: { error: "this worker requires a service auth token (set SERVICE_AUTH_TOKEN) before accepting task submissions", errorCode: "auth.service_token_required" } };
3350
2014
  }
@@ -3369,12 +2033,6 @@ export function createHttpServer(rawDeps) {
3369
2033
  },
3370
2034
  };
3371
2035
  }
3372
- // 🔴 A-058 F0(7.40 合并窗重扫,high,opus 证伪 CONFIRMED):第四道 = **成本配额门**。`/decide` 对
3373
- // 同一动作吃 `quotaExceeded`(approvals-assistant 路由挂);respond 路由刻意只有 rateLimited(活卡
3374
- // 回决=resolve 在飞 promise,确实无新计费,路由注释如实),但本席开的是一条新的烧模型 resume 腿——
3375
- // 修前超窗租户可以「攒一张会 park 的卡、park 后再赎」绕开成本上限。判据与文案逐字取 `quotaExceeded`
3376
- // 的同三件(gatedPrincipal 验证身份 / costQuota.check / 429 同码同文案);无 principal = not gated
3377
- // 是那道门自己的成文语义(单用户 turnkey 形),此处同判不另立。
3378
2036
  if (deps.costQuota && req.httpReq) {
3379
2037
  const quotaPrincipal = gatedPrincipal(req.httpReq, deps.config);
3380
2038
  if (quotaPrincipal !== undefined) {
@@ -3396,14 +2054,8 @@ export function createHttpServer(rawDeps) {
3396
2054
  return { status: 409, body: { error: e instanceof Error ? e.message : String(e), errorCode: "conflict.checkpoint_unreadable" } };
3397
2055
  }
3398
2056
  if (!cp) {
3399
- // 行说 PARKED,checkpoint 却没了(过期清算 / 已被消费)——answer 无处兑现。用与「本会话没有 pending」
3400
- // 同一句同一码(不给存在性谕示,与 decide 腿的 404 口径一致)。
3401
2057
  return { status: 404, body: { error: "approval not found", errorCode: "not_found.approval" } };
3402
2058
  }
3403
- // 🔴 属主门第三道:**checkpoint 当下的 scope**(codex R1-[high] 一的另一半)。判据逐字取 `/decide` 路由
3404
- // 的非 operator 支(`"_"`/null = 匿名单用户形,开放;否则必须与已验证调用方逐字相等)—— 本席绝不比那条
3405
- // 腿宽。收编把行与 cp 的身份一起改写之后,持旧 approvalId 的前属主到这里被挡下(协调器那边还有一道
3406
- // 行属主门,两道各判各的持久事实)。**不加 operator 越权臂**:活卡是终端用户自己的。
3407
2059
  const cpPrincipal = decodeCheckpointScope(cp.scope);
3408
2060
  if (cpPrincipal !== undefined && cpPrincipal !== req.principal) {
3409
2061
  deps.logger?.warn?.("parked_late_decision_scope_mismatch", { askId: req.askId });
@@ -3415,22 +2067,10 @@ export function createHttpServer(rawDeps) {
3415
2067
  ...(req.gateBoundInputHash !== null ? { boundInputHash: req.gateBoundInputHash } : {}),
3416
2068
  ...(req.updatedInput !== undefined ? { updatedInput: req.updatedInput } : {}),
3417
2069
  };
3418
- // #204 件6③:出处 = `"human"` —— 走到这里的恒是壳上那张卡被**人**按下的那一次 respond
3419
- // (窗到期/取消/SLA sweep 都不经本席)。
3420
2070
  const out = await resumeCheckpoint(cp.sessionId, req.decision, req.note, "human", req.httpReq, undefined, binding, undefined, true);
3421
2071
  return { status: out.status, body: out.body };
3422
2072
  }
3423
- /**
3424
- * #261 §1(a):**副本本地活体谓词** —— 这条 taskId 现在是不是本副本手上的活腿。
3425
- *
3426
- * 交出的是一个 `(taskId) => boolean` **探针**,不是 Map:登记面的属主仍是本模块(它们是 cancel /
3427
- * steer / preempt 的权威表),对账腿只问一个是非题。搬 Map 会造出第二份活性判据,而两份判据必漂移。
3428
- * 并集口径与 `drainState.inflight` 逐字同源(`inflightRuns` = 在飞 durable 腿、`steerableRuns` =
3429
- * 持活 core 流的腿;一条 resume 腿同时在两表里),所以「本副本认为它活着」在两条消费线上是同一句话。
3430
- */
3431
2073
  const isRunLiveOnThisReplica = (taskId) => inflightRuns.has(taskId) || steerableRuns.has(taskId);
3432
- // Augment the http.Server with the D-D deny-sweep handle (back-compat: all 12 callers keep using the return
3433
- // value AS an http.Server — listen/close/etc. — while main.ts's reaper reads server.denyExpiredApprovals).
3434
2074
  return Object.assign(server, { denyExpiredApprovals, isRunLiveOnThisReplica, redeemParkedAsk });
3435
2075
  }
3436
2076
  const WORKER_SWAP_REDEEMABLE = {
@@ -3439,114 +2079,37 @@ const WORKER_SWAP_REDEEMABLE = {
3439
2079
  governed_unwired: true,
3440
2080
  real_approval_damaged: false,
3441
2081
  real_approval_forged: false,
3442
- // core 5.30.0(#246 提货):v9 read-face 段的行完整性两词——d.ts 逐字与 real_approval 两词同列
3443
- // 「terminal for the row's current bytes」,换 worker 不可兑,归属同前(invalid_outcome 臂,行留 pending)。
3444
2082
  read_face_damaged: false,
3445
2083
  read_face_forged: false,
3446
2084
  constraint_chain_missing: false,
3447
2085
  };
3448
- /** 判别符缺席(旧引擎裸抛)或词表不认得 ⇒ **false**,即维持修前的 TERMINAL 归属。fail-closed 的方向在
3449
- * 这里是「不宽容默认」:一个我们还没读过的新臂不该因为一个 `?? true` 被悄悄改判成「重试吧」。 */
3450
2086
  function checkpointRowRedeemableElsewhere(reason) {
3451
2087
  return reason !== undefined && WORKER_SWAP_REDEEMABLE[reason] === true;
3452
2088
  }
3453
- /** POST endpoints that trigger BILLABLE work — the fail-closed auth guard must cover ALL of them (council: the
3454
- * guard's inline list had drifted from the handlers and missed `/v1/approvals/:id/decide`, which resumes a run
3455
- * via resumeCheckpoint/store.decide → paid tokens). Keep this in sync when adding a billable POST route. */
3456
- // [#104/#87-A3] exported for the declaration gate (test/billable-route-declaration.test.ts):每条路由
3457
- // 标签必须显式申明 billable,与本谓词逐样本对账——「Keep this in sync」从注释请求变成机器断言。
3458
- /**
3459
- * 「这条 url **是**计费提交面,但它到底提不提交由**请求体里的方法**决定」—— 目前只有 A2A 的
3460
- * JSON-RPC 端点(单 URL 承载 `message/send` 写 + `tasks/get` 读,因为 agent card 只能公告一个 `url`)。
3461
- *
3462
- * 用途:两道**可用性** 503(drain / model-roster-pending)按 url 一刀切会把 `tasks/get` 这条纯读一起
3463
- * 关掉,而单副本部署恰好在重启/依赖降级窗口里最需要取回已完成的结果(codex 轮2 R2-3,验真后修)。
3464
- * 本谓词让 `handle()` 对这条 url **跳过**那两道,改由域模块在 `message/send` 分支里施加同样的两道
3465
- * (`routes/a2a-serve.ts` 的 `submitUnavailable`,文案逐字同源)。
3466
- *
3467
- * 🔴 **只豁免那两道,不豁免安全轴**:`isBillableSubmitPath` 本身**不变**(它仍答 true),所以
3468
- * ① 无 service token 的部署上那道 fail-closed 503 照旧罩着这条 url —— 它判的是「这台机能不能收写」,
3469
- * 与方法无关,而且门必须在解析请求体**之前**;
3470
- * ② billable 申明门钉的仍是那只纯谓词(表里 `/v1/a2a` 依旧 billable=true)。
3471
- * 新成员进本表 = 承诺「该域在自己的方法分支里补齐那两道」,漏补就是一条静默旁路;所以本表只该有
3472
- * 「单 URL 多方法」这一种成员,加一条要连同它的域内补门一起过评审。
3473
- */
3474
2089
  export function isMethodDispatchedSubmitPath(url) {
3475
2090
  return url === "/v1/a2a";
3476
2091
  }
3477
- /** 「这台部署配了任何一把 service 凭证吗」—— fail-closed 写门族的真值源。**单一属主**:`handle()` 的
3478
- * 凭证门与 #329 的 parked 赎回席读同一句(两处手抄迟早只改一处,而这条判据决定的是「无凭证部署上写口
3479
- * 开不开」)。 */
3480
2092
  function hasServiceAuth(config) {
3481
2093
  return Boolean(config.authToken) || Object.keys(config.authTokens ?? {}).length > 0;
3482
2094
  }
3483
2095
  export function isBillableSubmitPath(url) {
3484
- return (url === "/v1/side-query" || // [1469] one-shot brain call — runs the model, so it rides every billable-submit gate
2096
+ return (url === "/v1/side-query" ||
3485
2097
  url === "/v1/tasks" ||
3486
2098
  url === "/v1/tasks/stream" ||
3487
2099
  url === "/v1/runs" ||
3488
2100
  url === "/v1/leader" ||
3489
- // DESIGN-269 车2:A2A JSON-RPC 端点。`message/send` 走的是与 /v1/runs **同一条**提交腿 ⇒ 它烧模型
3490
- // ⇒ 三道 503 门(drain / model-roster-pending / 无 service-token)必须对它生效,否则一台正在下线的
3491
- // 副本会照收外部 peer 的新任务(wake 当年漏在名单外犯的就是这一条)。
3492
- // ⚠️ 端点是**单 URL** 的(A2A 卡只公告一个 url),所以同一道门也会在排空期挡下 `tasks/get` 的读。
3493
- // 方向取「宁紧」:开新模型工作不可回滚,而读面有现成补偿(行是 durable 的,换一台副本读同一条)。
3494
2101
  url === "/v1/a2a" ||
3495
- ASSISTANT_RESUME_RE.test(url) || // resource_limit/preempt resume runs the model → BILLABLE (preempt is not)
3496
- ASSISTANT_PLAN_REVIEW_RE.test(url) || // plan_review approve/edit resumes + runs the model → BILLABLE
3497
- RUN_SUBAGENT_RESUME_RE.test(url) || // 对抗复查 B-2(HIGH):subagent revive (handle.resume) STARTS model work → BILLABLE
3498
- // 缝合审 2026-07-31:wake 走的是 assistant resume 那条同一个 `driveResumeIntoRunLog` 驱动腿
3499
- // (`routes/notify-wake.ts` 头注:「wake 会**烧模型**(续跑)」),resume 在名单里而它一直不在 ⇒
3500
- // 同一条模型腿从 wake 进来时 drain / model-roster-pending / 无 service-token 三道门全不生效。
3501
- // 正则从 notify-wake 单源导入,免得又漂开。钉在 test/drain.test.ts。
2102
+ ASSISTANT_RESUME_RE.test(url) ||
2103
+ ASSISTANT_PLAN_REVIEW_RE.test(url) ||
2104
+ RUN_SUBAGENT_RESUME_RE.test(url) ||
3502
2105
  SESSION_WAKE_RE.test(url) ||
3503
2106
  url.startsWith("/v1/approvals"));
3504
2107
  }
3505
- /**
3506
- * A-010.23(#209 件5)—— **改写门**:不烧模型、但在**没有任何 service credential** 的部署形下必须与
3507
- * billable / bake 两族一样 fail-closed 的写门。谓词与 `isBillableSubmitPath` **刻意分开**:这几扇门
3508
- * `billable=false`(零模型工作),塞进那张名单会让它们连带吃 drain 与 model-roster-pending 两道 503 ——
3509
- * 而收编与规则读写在排空期/roster 未落时做完全无害。
3510
- *
3511
- * 判据(两个合取项,缺一不进本表)= 「授权的唯一输入是 principal 头」∧「动作是**持久改写**」:
3512
- * · `POST /v1/adoption` —— operator-only 的一次性部署级动作,把一个 principal 名下**每一行**重写到另一个
3513
- * 身份;授权判据 `explicitOperatorOk(gatedPrincipal(...))`。
3514
- * · `POST /v1/rules/cc-import/*` —— principal lane 的**持久 allow 规则**铸造口;一条持久 allow 规则对后续
3515
- * 同命令的 classify 档 ask 常驻消音(core #144 之后仍然如此,只是不再消 mandated 的那些)。
3516
- * · `DELETE /v1/rules` —— 同一个规则店的**撤销**口。方向相反(收紧)但同样是持久改写,而且它多一条
3517
- * operator 越权域(`?principal=` / body 的 `principal` 让 operator 收回**任一**租户的规则)⇒ 伪造一个
3518
- * 列在 `OPERATOR_PRINCIPALS` 里的头,就能把每一位租户的规则一次清空。
3519
- * 无 service token 的部署上 `verifiedPrincipal` 走的是「BFF/gated:头已由上游验过」那一支,而这一形恰恰
3520
- * 是**没有**那个上游 —— 于是伪造一个头就能收编别人、替任意租户种下常驻放行、或替他们全删。bake 门当年
3521
- * 补的就是同一句话,`isDestructiveSessionWrite` 从逐条路由白名单改成族判定也是同一句话(那次的措辞逐字是
3522
- * 「a forged principal header alone could **tighten/DoS** a session's tools」——撤销口落的正是 DoS 那一半)。
3523
- *
3524
- * 🔴 **方法感知**(codex 对抗复审 R2-[high],验真后修):撤销走的是 `DELETE`,而本门最初只挂在 `POST` 的
3525
- * 合取里 —— 于是一个自称「持久改写」的谓词把爆炸半径最大的那条动词漏在门外,正是 `isDestructiveSessionWrite`
3526
- * 当年被抓到的同一形。签名因此收方法,与那只谓词逐字同形。
3527
- * 🔴 读面**原则上**不进本表:`GET /v1/adoption/:id` 与 `GET /v1/rules` 各有自己的**属主门**兜底,本门只拦写。
3528
- *
3529
- * 🔴 **第二个合取项的加宽**(codex 对抗复审 [high],#277 提货批,验真后修)——判据现在是
3530
- * 「持久改写 **或** 授权唯一输入是 principal 头的**跨租户治理读**」。加宽是为了收进 #264 v2-c 的两口:
3531
- * · `POST /v1/memory/import` —— 直接落在**原**判据上,而且是本仓最深的那一种持久改写:它把别处铸的
3532
- * 条目 / 挑战 / 污染标记 / 托管链落进本部署的记忆平面。伪造一个在册 operator 头 ⇒ 一份**攻击者自制的
3533
- * 治理史**成为本店的账;而记忆是模型**每次挂载都读**的面,比种一条常驻 allow 规则更深(那条只影响
3534
- * ask 会不会问,这条影响模型相信什么)。
3535
- * · `POST /v1/memory/export` —— 是**读**,按原措辞本不该进。但被点名豁免的那两条读各有属主门兜底,
3536
- * 而本条**没有**:它的授权判据就是 `explicitOperatorOk`,过了就是**全库**——一次调用取走每个 scope 的
3537
- * 条目正文 + 托管链 + 跨会话污染标记 + lineage。把一次全租户记忆外泄留在门外、只为守住「本门只拦写」
3538
- * 这句措辞,是把措辞看得比它要保护的东西更重。加宽后的判据仍然是**闭**的(不是「写门以外都进」):
3539
- * 它要求「授权的唯一输入是那个头」∧「一次调用的爆炸半径跨租户」,`GET /v1/rules` 那种属主门读不满足。
3540
- * 行为面钉:`test/rewrite-door-service-token-gate.test.ts`(六扇门各一正一反 + 逃生口格 + 读面负控)。
3541
- */
3542
2108
  export function isCredentialGatedRewrite(method, url) {
3543
2109
  const m = method.toUpperCase();
3544
2110
  if (m === "POST") {
3545
2111
  return (url === "/v1/adoption" ||
3546
2112
  url.startsWith("/v1/rules/cc-import/") ||
3547
- // #264 v2-c 的两口(codex 对抗复审 [high],验真后修)。判据见上方头注**加宽**的那一段:
3548
- // import 是本仓最深的持久改写(外来治理史成为本店的账,而记忆是模型每次挂载都读的面);
3549
- // export 虽是读,但它是**没有属主门兜底**的跨租户治理外泄——两者的授权唯一输入都是那个头。
3550
2113
  url === "/v1/memory/import" ||
3551
2114
  url === "/v1/memory/export");
3552
2115
  }
@@ -3554,13 +2117,6 @@ export function isCredentialGatedRewrite(method, url) {
3554
2117
  return url === "/v1/rules";
3555
2118
  return false;
3556
2119
  }
3557
- // ⚠️ SSE 帧纪律(2026-07-12):center BFF 中继凭「data 无 type 字段」识别心跳帧并吞掉
3558
- // (AgentEvent 是 closed oneof 全带 type,故可控)。**未来新增任何 SSE data 帧必须带 type 字段**,否则会被
3559
- // 中继当心跳吞掉、永远到不了前端。heartbeat 帧(`event: heartbeat` + `data: {}`)是唯一豁免。
3560
- /** 路由标签表(表驱动;[2061] 批1 层A 重写)。顺序=具体先于泛化。
3561
- * 🔴 本表由 `test/route-shape-roster.test.ts` 棘轮看守:门机器枚举源码全部路由模式并断言每条都映射到
3562
- * 非 "other"——新路由不补此表该门当场红(census 抓获本表曾漂掉 56 条模式:session-sync/workspace/
3563
- * workflows/subagents/attachments/images/leader 等整族全折 "other",metrics 不可分桶)。 */
3564
2120
  const ROUTE_LABEL_PATTERNS = [
3565
2121
  [/^\/v1\/runs\/[^/]+\/steer$/, "/v1/runs/:id/steer"],
3566
2122
  [/^\/v1\/runs\/[^/]+\/detach$/, "/v1/runs/:id/detach"],
@@ -3593,23 +2149,16 @@ const ROUTE_LABEL_PATTERNS = [
3593
2149
  [/^\/v1\/workflows\/[^/]+\/agents\/[^/]+\/steer$/, "/v1/workflows/:id/agents/:aid/steer"],
3594
2150
  [/^\/v1\/workflows\/[^/]+\/(stream|journal)$/, "/v1/workflows/:id/$sub"],
3595
2151
  [/^\/v1\/workflows\/[^/]+$/, "/v1/workflows/:id"],
3596
- // #151 车4:durable 回决口。必须排在 `/v1/tasks/:id/$sub` **之前**?—— 不必(那条只认三个字面动词),
3597
- // 但仍按「具体先于泛化」的表序纪律放在它前面,免得日后 $sub 放宽成 `[^/]+` 时静默吞掉本条。
3598
2152
  [/^\/v1\/tasks\/[^/]+\/asks\/[^/]+\/decision$/, "/v1/tasks/:id/asks/:askId/decision"],
3599
- // [3321] tool-results 读面:同样排在 `/v1/tasks/:id/$sub` 之前(具体先于泛化的表序纪律;$sub 那条
3600
- // 只认三个字面动词,今日不冲突,但它日放宽成 `[^/]+` 时本条不会被静默吞掉)。
3601
2153
  [/^\/v1\/tasks\/[^/]+\/tool-results\/[^/]+$/, "/v1/tasks/:id/tool-results/:ref"],
3602
2154
  [/^\/v1\/tasks\/[^/]+\/(turns|stream|artifacts)$/, "/v1/tasks/:id/$sub"],
3603
2155
  [/^\/v1\/leader\/[^/]+$/, "/v1/leader/:id"],
3604
2156
  [/^\/v1\/attachments\/[^/]+$/, "/v1/attachments/:id"],
3605
2157
  [/^\/v1\/capabilities\/scenarios\/[^/]+$/, "/v1/capabilities/scenarios/:name"],
3606
2158
  [/^\/v1\/adoption\/[^/]+$/, "/v1/adoption/:id"],
3607
- // #270 车2:留存 legal-hold 的放置/解除。`*` 不是 `+` —— 空段 = 无主桶(单用户部署里它是唯一的域;
3608
- // 理由逐字见 routes/retention-ops.ts 的「域的两个边界」)。
3609
2159
  [/^\/v1\/ops\/retention\/holds\/[^/]*$/, "/v1/ops/retention/holds/:domain"],
3610
2160
  [/^\/v1\/memory\/sync\/[^/]+$/, "/v1/memory/sync/:scope"],
3611
2161
  [/^\/v1\/usage\/(summary|series|breakdown)$/, "/v1/usage/$sub"],
3612
- // design/177 共享记忆库只读面。两条各自成桶(列举是窗口读、正文是单文档读,延迟分布不同族)。
3613
2162
  [/^\/v1\/shared-memory\/stores\/[^/]+\/documents$/, "/v1/shared-memory/stores/:store/documents"],
3614
2163
  [/^\/v1\/shared-memory\/stores\/[^/]+\/document$/, "/v1/shared-memory/stores/:store/document"],
3615
2164
  ];
@@ -3620,55 +2169,20 @@ const ROUTE_LABEL_LITERALS = new Set([
3620
2169
  "/v1/assistant/inbox", "/v1/assistant/tasks", "/v1/usage", "/v1/policy", "/v1/capabilities",
3621
2170
  "/v1/images", "/v1/images/bakes", "/v1/images/bakes/claim", "/v1/images/select", "/v1/images/register",
3622
2171
  "/v1/workflows", "/v1/attachments", "/v1/leader", "/v1/outcomes", "/v1/side-query",
3623
- // 2026-07-31 缝合审:它一直不在表里,而它是**每个壳一条的长连接** —— 那种 duration 落进 `other`
3624
- // 桶会把该桶的 P99 拉爆,运营看不出是谁。批1 建表时漏掉它不是遗忘:fleet 域用 `pathname === …` 判路,
3625
- // 名册门的枚举器当时只认 `url === …`,整条路由对门隐形(枚举器已一并补上)。
3626
2172
  "/v1/fleet/stream",
3627
- // ⚠️ `/v1/memory/export` 是**两个面**共用的标签(GET=142-S5 属主自助条目读,POST=#264 治理包)——
3628
- // 标签粒度粗于面的边界,但两面同 billable(false)、同为低频运维/壳读,合桶不会掩盖任何一方的信号。
3629
2173
  "/v1/memory/export", "/v1/sendfile-links",
3630
- // #264 v2-c:治理包的导入口(operator-only,POST)。低频,但它是**治理写面** —— 它的 4xx/5xx 分布
3631
- // 是「这批迁移到底成没成」的第一手机器信号,落进 `other` 桶等于迁移现场看不见它。同族纪律:写
3632
- // **字面量**不引常量(名册门与 billable 申明门都扫源码文本)。
3633
2174
  "/v1/memory/import",
3634
- // design/177:共享记忆库清单面(具名两条走上面的模式表)。
3635
2175
  "/v1/shared-memory/stores",
3636
- // #154:装配自证读面(operator-only)。低频但**每次都在排障现场被打**,落进 `other` 桶等于排障时看不见。
3637
2176
  "/v1/diagnostics/wiring",
3638
- // design/183:收编发起口(operator-only,一次性部署级动作)。低频、但它的 duration 与失败率是
3639
- // 迁移现场唯一的机器信号,落进 `other` 桶就看不见了。
3640
2177
  "/v1/adoption",
3641
- // #291:停机因由声明口(operator-only,drain 前一次性动作)。同 adoption 判:低频但只在停机现场被打。
3642
2178
  "/v1/admin/drain",
3643
- // #322 批1 追加件:配置手动刷新口(operator-only)。同判:低频,但它的 duration 与失败率正是「这次
3644
- // 配置到底拉没拉下来」的第一手信号,落进 other 桶就看不见了。同族纪律:写**字面量**不引常量。
3645
2179
  "/v1/admin/config/refresh",
3646
- // #154 车二:CC settings 导入两口。字面量(无 id 段)⇒ 进 LITERALS 表而不是模式表。
3647
- // 🔴 写**字面量**而不是引 `RULES_CC_IMPORT_*_PATH` 常量:名册门与 billable 申明门都是**扫源码文本**
3648
- // 的(它们正则抓本表里的双引号词),引常量会让两条新路由对两道门隐形 —— 那不是「过了门」,是绕过门。
3649
- // ⚠️ 本段注释里也不许出现方括号后紧跟右圆括号的字符对:billable 门的表捕获正则是**非贪婪**的,
3650
- // 那两个字符会让它在这里提前收尾,后面的字面量整段落在门的视野外(写这条注时真的踩到过)。
3651
2180
  "/v1/rules/cc-import/prepare", "/v1/rules/cc-import/redeem",
3652
- // #203 §2:撤销面两口(GET 列举 / DELETE 撤销)共用一个字面路径。同样写字面量而不是引 `RULES_PATH`
3653
- // 常量,理由与上一条逐字相同(名册门与 billable 申明门都扫源码文本)。
3654
2181
  "/v1/rules",
3655
- // DESIGN-269 车2:server-as-peer 两口。卡是**公网匿名**面(唯一一条不吃服务凭据的 2xx 路由),
3656
- // 它的 QPS 与 duration 是判断「有没有人在扫我们」的第一手材料;RPC 口是外部 peer 的提交面。
3657
- // 两条都落进 `other` 桶 = 对外暴露面在 metrics 上不可分。同上,写**字面量**不引常量(两道门扫源码文本)。
3658
2182
  "/.well-known/agent-card.json", "/v1/a2a",
3659
- // #A1([4147] 件3):后台 agent 名册读面。壳每次开面板打一次,是**每用户高频**的租户读 ——
3660
- // 落进 `other` 桶会把它的 duration 和 4xx 混进一堆低频运维口里。同上,写字面量不引常量。
3661
2183
  "/v1/agents/roster",
3662
- // #270 车2:留存审计读面(operator-only)。低频,但它是「留存到底删没删、按哪档删的」的唯一读口
3663
- // —— 落进 `other` 桶等于合规排障现场看不见它。同上,写**字面量**不引常量(名册门与 billable
3664
- // 申明门都扫源码文本)。
3665
2184
  "/v1/ops/retention/audit",
3666
2185
  ]);
3667
- /** Stable, low-cardinality route label for metrics/logs (ids collapsed to `:id`).
3668
- * [#104] 字面量**先于**模式:此前模式先查,五条精确路由被形状桶吞掉(`/v1/approvals/stream`
3669
- * 落 ":id"、`/v1/images/{bakes,select,register}` 落 ":profile"、`/v1/images/bakes/claim` 落
3670
- * "bakes/:id")——长连接 stream 的 duration 混进 :id 桶正是 fleet/stream 案的同族错桶病。
3671
- * 精确匹配恒比形状匹配更对,序修零反例(billable-route-declaration 门逐样本钉)。 */
3672
2186
  export function routeLabel(_method, url) {
3673
2187
  if (ROUTE_LABEL_LITERALS.has(url))
3674
2188
  return url;
@@ -3677,13 +2191,9 @@ export function routeLabel(_method, url) {
3677
2191
  return label;
3678
2192
  return "other";
3679
2193
  }
3680
- /** CORS v2: allowlist semantics. A SINGLE configured origin keeps the v1 posture
3681
- * (echoed unconditionally — non-browser callers see it too, byte-compat); MULTIPLE origins match the
3682
- * request's Origin header (echo the matched one, `Vary: Origin` so a shared cache never serves origin-A's
3683
- * grant to origin-B; no match ⇒ no allow-origin header ⇒ the browser refuses). Empty list = CORS off. */
3684
2194
  function cors(res, req, origins, principalHeader) {
3685
2195
  if (!origins || origins.length === 0)
3686
- return; // defensive undefined: a hand-built test config may omit the field
2196
+ return;
3687
2197
  let allow;
3688
2198
  if (origins.length === 1) {
3689
2199
  allow = origins[0];
@@ -3696,23 +2206,8 @@ function cors(res, req, origins, principalHeader) {
3696
2206
  if (allow === undefined)
3697
2207
  return;
3698
2208
  res.setHeader("access-control-allow-origin", allow);
3699
- // 🔴 这三行自 2026-07-14 公开快照起逐字未动,而这期间进来的路由与请求头它一个都不认
3700
- // (2026-08-01 缝合审)。浏览器前端撞的不是"请求失败"而是**请求根本发不出去** —— OPTIONS 预检就被拒。
3701
- // 维护纪律:**新增 PUT/DELETE 路由、或新读一个自定义请求头时,这里必须同步**;
3702
- // `test/cors-v2.test.ts` 的三条钉逐个断言方法与头名,漏一个当场红。
3703
2209
  res.setHeader("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS");
3704
- // Include the principal header so a browser front-end that sends it isn't rejected by the OPTIONS preflight
3705
- // (which would otherwise drop identity → an owner=null session under requirePrincipal=false).
3706
- // 后五项都是**真被 handler 读**的自定义头:`idempotency-key`(tasks/runs/images)、
3707
- // `x-detach-on-disconnect`(tasks 断连不中止)、direct-door 决策证明三件套(approvals /decide 的
3708
- // crypto 绑定腿 —— 缺了它,direct-door worker 上的 web 审批面在浏览器里根本用不了)。
3709
- res.setHeader("access-control-allow-headers",
3710
- // direct-door 三件套用 security.ts 的具名导出插值(与同句的 `${principalHeader}` 同姿势)——手抄的
3711
- // 头名漏改在这一行是**静默**的:预检挡下 ⇒ 请求到不了 handler,任何一处都不会报错。
3712
- `content-type, authorization, last-event-id, ${principalHeader}, idempotency-key, x-detach-on-disconnect, ${PRINCIPAL_TOKEN_HEADER}, ${APPROVAL_MAC_HEADER}, ${APPROVAL_MAC_KID_HEADER}, if-none-match`);
3713
- // 🔴 expose-headers 此前**整个缺席** ⇒ fetch 类客户端读不到 `X-Task-Id`(routes/tasks.ts 经 sseHeaders
3714
- // 下发的 durable rewind handle)。tasks.ts 的 G15 meta 帧注释把这归因为「EventSource 与部分代理读不到
3715
- // 响应头」—— 对 EventSource 成立,但对 fetch 客户端真因是这里缺 expose,meta 帧只是绕过。
2210
+ res.setHeader("access-control-allow-headers", `content-type, authorization, last-event-id, ${principalHeader}, idempotency-key, x-detach-on-disconnect, ${PRINCIPAL_TOKEN_HEADER}, ${APPROVAL_MAC_HEADER}, ${APPROVAL_MAC_KID_HEADER}, if-none-match`);
3716
2211
  res.setHeader("access-control-expose-headers", "x-task-id, retry-after");
3717
2212
  }
3718
2213
  //# sourceMappingURL=server.js.map