@sema-agent/server 7.43.0 → 7.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/USAGE.md +18 -0
  2. package/dist/adoption/plan.js +31 -192
  3. package/dist/adoption/quiesce.js +3 -86
  4. package/dist/adoption/runner.js +3 -140
  5. package/dist/adoption/sql.js +0 -74
  6. package/dist/adoption/wire.js +0 -80
  7. package/dist/approval-ask-machine.js +0 -75
  8. package/dist/approval-card.js +0 -323
  9. package/dist/approval-deny-reasons.js +0 -42
  10. package/dist/approval-hmac.js +0 -35
  11. package/dist/approval-reconciler.js +8 -166
  12. package/dist/approval.js +4 -67
  13. package/dist/audit.js +1 -44
  14. package/dist/auth-bridge.js +4 -47
  15. package/dist/auth-keys.js +0 -23
  16. package/dist/bake-runner/main.js +4 -65
  17. package/dist/bake-runner/protocol.js +3 -58
  18. package/dist/bake-runner/runner.js +5 -91
  19. package/dist/bench/l8/artifact.js +2 -61
  20. package/dist/bench/l8/escape.js +0 -25
  21. package/dist/bench/l8/index.js +0 -14
  22. package/dist/bench/l8/probes.js +0 -60
  23. package/dist/bench/l8/run-probes.js +2 -45
  24. package/dist/bench/s1/arms.js +14 -189
  25. package/dist/bench/s1/live-deps.js +11 -253
  26. package/dist/bench/s1/oracle.js +1 -36
  27. package/dist/bench/s1/repair-oracle-adapter.js +0 -30
  28. package/dist/bench/s1/reviewer.js +0 -34
  29. package/dist/bench/s1/row.js +0 -62
  30. package/dist/bench/s1/run-firm.js +4 -70
  31. package/dist/bench/s1/runner-ctx.js +0 -40
  32. package/dist/bench/s1/tasks.js +0 -115
  33. package/dist/boot/adoption.js +0 -20
  34. package/dist/boot/budget-tracing.js +3 -55
  35. package/dist/boot/config-center.js +43 -551
  36. package/dist/boot/coordinators.js +1 -69
  37. package/dist/boot/crash-last.js +0 -18
  38. package/dist/boot/deferred-sandbox-path-env.js +3 -119
  39. package/dist/boot/execution-env.js +5 -171
  40. package/dist/boot/governance-seams.js +0 -92
  41. package/dist/boot/leader.js +0 -69
  42. package/dist/boot/limit-sync.js +0 -2
  43. package/dist/boot/memory-boundary.js +3 -91
  44. package/dist/boot/org-memory.js +1 -25
  45. package/dist/boot/parked-revive-gate.js +0 -150
  46. package/dist/boot/permission-rules-audit.js +1 -56
  47. package/dist/boot/reapers.js +15 -273
  48. package/dist/boot/resolve-spec.js +9 -768
  49. package/dist/boot/retention-lane.js +4 -87
  50. package/dist/boot/runner-deps.js +3 -297
  51. package/dist/boot/runtime-caps.js +0 -40
  52. package/dist/boot/session-faces.js +2 -133
  53. package/dist/boot/shutdown.js +9 -83
  54. package/dist/boot/side-query-lane.js +2 -137
  55. package/dist/boot/stores.js +15 -215
  56. package/dist/boot/task-list-lane.js +0 -18
  57. package/dist/boot/webfetch-summarize-lane.js +0 -47
  58. package/dist/boot/workflow-orchestration.js +3 -99
  59. package/dist/boot-reclaim.js +0 -20
  60. package/dist/bounded-session-map.js +0 -19
  61. package/dist/brain.js +2 -139
  62. package/dist/budget.js +9 -244
  63. package/dist/capabilities/builtin-tools.js +0 -3
  64. package/dist/capabilities/center-plugins.js +6 -51
  65. package/dist/capabilities/center-prompts.js +6 -71
  66. package/dist/capabilities/code-review-council.js +7 -40
  67. package/dist/capabilities/collab-workflows.js +1 -44
  68. package/dist/capabilities/hands-lane.js +0 -65
  69. package/dist/capabilities/memory-notice.js +0 -70
  70. package/dist/capabilities/prompt.js +0 -14
  71. package/dist/capabilities/prompts/code-review.js +0 -14
  72. package/dist/capabilities/prompts/identity.js +0 -6
  73. package/dist/capabilities/prompts/team.js +0 -4
  74. package/dist/capabilities/repo-tools.js +0 -34
  75. package/dist/capabilities/sandbox-file-send.js +3 -92
  76. package/dist/capabilities/scenarios.d.ts +0 -1
  77. package/dist/capabilities/scenarios.js +18 -279
  78. package/dist/capabilities/select-environment-tool.js +0 -32
  79. package/dist/capabilities/send-user-file-tool.js +2 -66
  80. package/dist/capabilities/skills.d.ts +1 -2
  81. package/dist/capabilities/skills.js +3 -21
  82. package/dist/capabilities/team.d.ts +3 -11
  83. package/dist/capabilities/team.js +3 -51
  84. package/dist/capabilities/tool-defer.js +0 -3
  85. package/dist/config-center/apply-effective.js +15 -330
  86. package/dist/config-center/apply-ledger.js +5 -38
  87. package/dist/config-center/facade.js +0 -41
  88. package/dist/config-center/hot-keys-registry.js +0 -20
  89. package/dist/config-center/http-client.js +2 -125
  90. package/dist/config-center/mcp-revocation.js +2 -34
  91. package/dist/config-center/read-face.js +0 -59
  92. package/dist/config-center/restart-signal.js +1 -85
  93. package/dist/config-center/skills-mcp.d.ts +1 -1
  94. package/dist/config-center/skills-mcp.js +9 -76
  95. package/dist/config-center/stage-limits.js +8 -35
  96. package/dist/config-invariants.js +0 -16
  97. package/dist/config-lkg.js +0 -42
  98. package/dist/config-provider.js +3 -186
  99. package/dist/config-types.js +0 -5
  100. package/dist/config.js +107 -1145
  101. package/dist/degenerate-instrument.js +3 -67
  102. package/dist/deployment-governance.js +0 -124
  103. package/dist/digest-form.js +0 -12
  104. package/dist/elicitation.js +3 -86
  105. package/dist/env-facts.js +7 -75
  106. package/dist/fleet/fleet-bus.js +34 -507
  107. package/dist/fleet/fleet-reconciler.js +9 -149
  108. package/dist/fleet/fleet-terminal-window.js +10 -178
  109. package/dist/fleet/subagent-tail-bus.js +3 -72
  110. package/dist/fleet-client.js +10 -70
  111. package/dist/fleet-lease.js +5 -79
  112. package/dist/git-api-kind.js +0 -3
  113. package/dist/governance-ask-marks.js +2 -78
  114. package/dist/hooks/branch-transcript.js +0 -74
  115. package/dist/hooks/cc-agent-hook-prompt.js +0 -29
  116. package/dist/hooks/cc-stop-prompt.js +1 -46
  117. package/dist/hooks/hook-llm.js +1 -53
  118. package/dist/hooks/hook-runner.js +20 -414
  119. package/dist/http/active-run-conflict.js +4 -129
  120. package/dist/http/cursor-fingerprint.d.ts +5 -0
  121. package/dist/http/cursor-fingerprint.js +5 -0
  122. package/dist/http/idempotency.js +0 -37
  123. package/dist/http/principal-gate.js +3 -40
  124. package/dist/http/route-ctx.js +0 -9
  125. package/dist/http/routes/a2a-serve.js +7 -319
  126. package/dist/http/routes/admin-config-refresh.js +0 -4
  127. package/dist/http/routes/admin-drain.js +0 -10
  128. package/dist/http/routes/adoption.js +1 -29
  129. package/dist/http/routes/agents-roster.js +1 -47
  130. package/dist/http/routes/approvals-assistant.js +32 -395
  131. package/dist/http/routes/attachments.js +4 -22
  132. package/dist/http/routes/capabilities.js +3 -420
  133. package/dist/http/routes/diagnostics.js +1 -81
  134. package/dist/http/routes/fleet.js +9 -185
  135. package/dist/http/routes/images.js +14 -239
  136. package/dist/http/routes/leader.js +0 -13
  137. package/dist/http/routes/memory-bundle.js +1 -61
  138. package/dist/http/routes/memory-policy.js +9 -99
  139. package/dist/http/routes/notify-wake.js +3 -37
  140. package/dist/http/routes/observability.js +2 -19
  141. package/dist/http/routes/retention-ops.js +2 -34
  142. package/dist/http/routes/rules.js +1 -93
  143. package/dist/http/routes/runs.js +64 -873
  144. package/dist/http/routes/session-sync.js +19 -258
  145. package/dist/http/routes/sessions-list.js +17 -43
  146. package/dist/http/routes/sessions.js +34 -226
  147. package/dist/http/routes/shared-memory.js +5 -36
  148. package/dist/http/routes/side-query.js +1 -87
  149. package/dist/http/routes/tasks.js +52 -722
  150. package/dist/http/routes/trace-usage.js +32 -195
  151. package/dist/http/routes/workflows.js +19 -195
  152. package/dist/http/run-meta.js +0 -6
  153. package/dist/http/send.js +0 -32
  154. package/dist/http/server.js +92 -1597
  155. package/dist/http/sse-lifecycle.js +2 -13
  156. package/dist/http/sse-log.js +3 -48
  157. package/dist/http/tar.js +5 -21
  158. package/dist/http/verify-rounds.js +0 -5
  159. package/dist/http/wire-gate.js +0 -9
  160. package/dist/http/workspace-content.js +0 -10
  161. package/dist/images/bake-validate.js +1 -70
  162. package/dist/images/manifest.js +1 -6
  163. package/dist/index.js +0 -21
  164. package/dist/key-resolver.js +2 -17
  165. package/dist/leader/diffout.js +1 -20
  166. package/dist/leader/diffup.js +0 -47
  167. package/dist/leader/endpoint.js +2 -57
  168. package/dist/leader/fanout.js +3 -45
  169. package/dist/leader/grader-env-factory.js +3 -72
  170. package/dist/leader/leader.js +5 -156
  171. package/dist/leader/merge.js +8 -115
  172. package/dist/leader/planner.js +3 -54
  173. package/dist/leader/repair-oracle.js +1 -60
  174. package/dist/leader/repair-wire.js +2 -79
  175. package/dist/leader/wire.js +8 -307
  176. package/dist/lsp/e2b-bridge.js +4 -64
  177. package/dist/lsp/e2b-manager.js +6 -94
  178. package/dist/lsp/lsp-frames.js +0 -12
  179. package/dist/lsp/manager.js +4 -96
  180. package/dist/lsp/ws-transport.js +5 -55
  181. package/dist/lsp-evict.js +1 -15
  182. package/dist/main.js +57 -765
  183. package/dist/memory-bundle-engine.js +0 -55
  184. package/dist/memory-export.js +0 -4
  185. package/dist/memory-posture.js +1 -15
  186. package/dist/memory-scope.js +11 -148
  187. package/dist/memory-sync-client.js +2 -44
  188. package/dist/memory-sync.js +1 -80
  189. package/dist/model-select.js +3 -80
  190. package/dist/observability/cost-quota.js +1 -17
  191. package/dist/observability/cost-taxonomy.js +0 -34
  192. package/dist/observability/fail-open.js +7 -86
  193. package/dist/observability/logger.js +0 -6
  194. package/dist/observability/metrics.js +0 -94
  195. package/dist/observability/otel-exporter.js +3 -13
  196. package/dist/observability/principal-context.js +0 -9
  197. package/dist/observability/prompt-manifest.js +1 -37
  198. package/dist/observability/rate-limit.js +0 -4
  199. package/dist/observability/secret-env-scrub.js +2 -56
  200. package/dist/observability/tool-trace.js +1 -70
  201. package/dist/orchestration/hardened-vm-runner.js +4 -118
  202. package/dist/orchestration/hardened-vm-worker-runner.js +1 -26
  203. package/dist/orchestration/hardened-vm-worker.js +0 -27
  204. package/dist/orchestration/subagent-steer.js +1 -45
  205. package/dist/orchestration/workflow-agent-steer.js +1 -80
  206. package/dist/orchestration/workflow-completion-inbox.js +32 -285
  207. package/dist/orchestration/workflow-notify-journal.js +16 -259
  208. package/dist/org-memory-admission.js +3 -47
  209. package/dist/parent-watch.js +2 -48
  210. package/dist/parked-decide.js +1 -109
  211. package/dist/per-task-image.js +0 -57
  212. package/dist/plan-cache-probe.js +3 -27
  213. package/dist/plugins/adoption-log-sql.js +2 -119
  214. package/dist/plugins/approval-ask-store-memory.js +3 -38
  215. package/dist/plugins/approval-ask-store-sql.js +6 -188
  216. package/dist/plugins/approval-exemption-store.js +2 -28
  217. package/dist/plugins/background-agent-store-sql.js +4 -105
  218. package/dist/plugins/background-shell-support.js +14 -122
  219. package/dist/plugins/blob-backend.js +6 -169
  220. package/dist/plugins/breaker-state-sql.js +8 -46
  221. package/dist/plugins/caching-session-store.js +4 -106
  222. package/dist/plugins/checkpoint-store-sql.js +18 -505
  223. package/dist/plugins/e2b-orphan-reclaim.js +0 -45
  224. package/dist/plugins/file-outcome-sink.js +0 -9
  225. package/dist/plugins/file-resume-anchor-store.js +4 -43
  226. package/dist/plugins/file-run-store.js +26 -364
  227. package/dist/plugins/file-snapshot-store-sql.js +10 -181
  228. package/dist/plugins/fork-routing-session-store.js +8 -111
  229. package/dist/plugins/host-platform.js +2 -91
  230. package/dist/plugins/image-bake-store-sql.js +7 -250
  231. package/dist/plugins/image-index-sql.js +4 -123
  232. package/dist/plugins/k8s-bg-scripts.js +4 -88
  233. package/dist/plugins/k8s-exec-protocol.js +0 -41
  234. package/dist/plugins/leader-run-store-sql.js +0 -101
  235. package/dist/plugins/local-checkpoint-store.js +8 -128
  236. package/dist/plugins/local-session-store.js +34 -296
  237. package/dist/plugins/local-task-attachment-store.js +2 -16
  238. package/dist/plugins/mailbox-store-sql.js +8 -65
  239. package/dist/plugins/memory-embedder-fingerprint.js +5 -166
  240. package/dist/plugins/memory-embedder.js +4 -70
  241. package/dist/plugins/memory-engine-pg.js +6 -170
  242. package/dist/plugins/memory-engine-tidb.js +7 -155
  243. package/dist/plugins/memory-engine-vector-util.js +0 -10
  244. package/dist/plugins/memory-key-guards.js +0 -34
  245. package/dist/plugins/memory-origin-law.js +0 -187
  246. package/dist/plugins/memory-resume-anchor-store.js +0 -17
  247. package/dist/plugins/memory-run-store.js +12 -89
  248. package/dist/plugins/memory-session-policy-store.js +0 -17
  249. package/dist/plugins/memory-sync-store-pg.js +4 -49
  250. package/dist/plugins/memory-sync-store-tidb.js +3 -35
  251. package/dist/plugins/outcome-ledger-sql.js +3 -97
  252. package/dist/plugins/permission-rule-store-file.js +5 -133
  253. package/dist/plugins/permission-rule-store-sql.d.ts +3 -0
  254. package/dist/plugins/permission-rule-store-sql.js +50 -307
  255. package/dist/plugins/pg-cost-quota.js +0 -7
  256. package/dist/plugins/pg-pool.js +0 -92
  257. package/dist/plugins/pg-rate-limiter.js +2 -13
  258. package/dist/plugins/pg-safe-json.js +4 -40
  259. package/dist/plugins/pg-session-storage.js +25 -189
  260. package/dist/plugins/posix-shell-fs.js +1 -31
  261. package/dist/plugins/remote-env-adb.js +12 -101
  262. package/dist/plugins/remote-env-e2b.js +40 -371
  263. package/dist/plugins/remote-env-file-error.js +0 -37
  264. package/dist/plugins/remote-env-host.js +63 -483
  265. package/dist/plugins/remote-env-k8s.js +32 -326
  266. package/dist/plugins/remote-env-local-docker.js +24 -157
  267. package/dist/plugins/remote-env-ssh.js +20 -128
  268. package/dist/plugins/remote-scratchpad.js +2 -32
  269. package/dist/plugins/remote-shell.js +1 -32
  270. package/dist/plugins/resume-anchor-store-sql.js +0 -11
  271. package/dist/plugins/retention-lane-store-sql.js +0 -108
  272. package/dist/plugins/retention-store-sql.js +4 -383
  273. package/dist/plugins/roster-store-sql.js +0 -55
  274. package/dist/plugins/run-store-sql.js +14 -251
  275. package/dist/plugins/s3-presign.js +2 -49
  276. package/dist/plugins/scheduler-support.js +3 -80
  277. package/dist/plugins/send-file-ledger.js +4 -53
  278. package/dist/plugins/send-user-file.js +4 -94
  279. package/dist/plugins/session-placement.js +1 -89
  280. package/dist/plugins/session-policy-store-sql.js +3 -81
  281. package/dist/plugins/session-store.js +0 -57
  282. package/dist/plugins/shared-memory-store-sql.js +4 -167
  283. package/dist/plugins/sql-driver.js +0 -17
  284. package/dist/plugins/sql-errors.js +0 -7
  285. package/dist/plugins/sql-escape.js +0 -8
  286. package/dist/plugins/sql-row-helpers.js +0 -25
  287. package/dist/plugins/store-backend.js +42 -222
  288. package/dist/plugins/store-contracts.js +2 -46
  289. package/dist/plugins/task-attachment-store.js +3 -49
  290. package/dist/plugins/task-list-store-sql.js +0 -79
  291. package/dist/plugins/tidb-cost-quota.js +1 -4
  292. package/dist/plugins/tidb-pool.js +1 -207
  293. package/dist/plugins/tidb-rate-limiter.js +3 -9
  294. package/dist/plugins/tidb-session-storage.js +4 -70
  295. package/dist/plugins/tidb-session-store.js +19 -341
  296. package/dist/plugins/tool-result-store-sql.js +4 -179
  297. package/dist/plugins/usage-window-store-sql.js +0 -8
  298. package/dist/plugins/web-search.js +10 -132
  299. package/dist/plugins/workflow-journal-store-sql.js +2 -58
  300. package/dist/plugins/workflow-run-store-sql.js +6 -91
  301. package/dist/plugins/worktree-isolation.js +6 -126
  302. package/dist/plugins/write-behind-counter.js +16 -75
  303. package/dist/principal-jwt.js +5 -60
  304. package/dist/project-memory.js +15 -146
  305. package/dist/prompts-domain-validate.js +1 -51
  306. package/dist/question.js +2 -111
  307. package/dist/resource-suspend.js +0 -18
  308. package/dist/router/route-orchestration.js +0 -77
  309. package/dist/rules-consent.d.ts +44 -5
  310. package/dist/rules-consent.js +75 -215
  311. package/dist/run-local.js +14 -381
  312. package/dist/runs.js +29 -617
  313. package/dist/runtime-caps-resolver.js +6 -132
  314. package/dist/runtime-governance.js +1 -232
  315. package/dist/sandbox-pkg-source.js +0 -37
  316. package/dist/sealed-key.js +3 -68
  317. package/dist/security.js +6 -291
  318. package/dist/session-leaf-bus.js +0 -32
  319. package/dist/session-sync-content.js +1 -69
  320. package/dist/session-sync-kernel.js +3 -60
  321. package/dist/session-sync.js +3 -66
  322. package/dist/session-titler.js +7 -41
  323. package/dist/session-watch.js +9 -73
  324. package/dist/shared-memory-scope-authorizer.js +0 -18
  325. package/dist/sighup-idle.js +1 -12
  326. package/dist/spec-fields.js +4 -143
  327. package/dist/store-live-probe.js +3 -49
  328. package/dist/task-a2a.js +1 -126
  329. package/dist/task-cwd.js +1 -103
  330. package/dist/task-mcp.js +1 -89
  331. package/dist/task-settings.js +10 -300
  332. package/dist/task-workflow.js +8 -75
  333. package/dist/tool-approval.d.ts +28 -1
  334. package/dist/tool-approval.js +70 -1430
  335. package/dist/trace/artifacts.js +5 -20
  336. package/dist/trace/engine-notice-wire.js +6 -138
  337. package/dist/trace/ledger-sink.js +7 -96
  338. package/dist/trace/project.js +4 -448
  339. package/dist/trace/redact.js +9 -77
  340. package/dist/turn-activity.js +1 -27
  341. package/dist/usage-analytics.js +4 -34
  342. package/dist/wall-clock-jump-guard.js +1 -68
  343. package/package.json +3 -3
  344. package/dist/capabilities/scenario-alias.d.ts +0 -27
  345. package/dist/capabilities/scenario-alias.js +0 -61
@@ -1,15 +1,3 @@
1
- /**
2
- * design/158 A10:composition root 分段 —— session 只读/删除面(审计、watch 注册表、E21 purge 协调器、
3
- * degenerate 仪表、plan-cache 探针)。
4
- *
5
- * 纯搬运:函数体逐字来自 `main.ts`(原 2967-3092 行),缩进不变;新增的只有 import 与包壳。
6
- *
7
- * ⚠️ **位置即契约**:
8
- * 1. `sessionWatchRegistry` 必须在 `purgeSession` **之前**构造 —— purge 尾部的 `dropSession` 是
9
- * 同副本租户轮转围栏(delete 即终结旧 owner 的 SSE 订阅),两者是一对。
10
- * 2. `setLeafAdvanceListener(...)` 是**进程级单点**装载(leaf-bus 只有一个监听位),必须与
11
- * registry 的构造同段发生,晚于它、早于任何会 append 的路由。
12
- */
13
1
  import { buildSessionAudit, buildSessionAuditLocal, buildSessionAuditPg } from "../audit.js";
14
2
  import { makeDegenerateInstrument } from "../degenerate-instrument.js";
15
3
  import { PlanCacheProbe } from "../plan-cache-probe.js";
@@ -19,11 +7,9 @@ import { SessionWatchRegistry, posIntEnv } from "../session-watch.js";
19
7
  import { defaultTaskRegistry } from "@sema-agent/core";
20
8
  export function createSessionFaces(ctx) {
21
9
  const { config, logger, metrics, localRoot, backend, sessionStore, runStore, checkpointStore, toolResultStore, resumeAnchorStore, approvalExemptionStore, sessionPolicyStore, taskAttachmentStore, fileSnapshotStore, workflowCompletionInbox, taskListLane, } = ctx;
22
- // Audit回溯 + degenerate-output instrument are TiDB-specific raw-SQL consumers (outside the store
23
- // abstraction); they run only on the TiDB backend (the mysql2 pool), and are a no-op on PG until ported.
24
10
  const mysqlPool = backend?.mysqlPool();
25
- const auditPgPool = backend?.pgPool(); // the audit face now has a PG twin — web session area lights up on DB_BACKEND=pg
26
- const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined; // the File leg
11
+ const auditPgPool = backend?.pgPool();
12
+ const auditLocalRoot = backend?.kind === "local" ? (config.localDataRoot ?? localRoot) : undefined;
27
13
  const ownerAware = sessionStore;
28
14
  const sessionAudit = mysqlPool || auditPgPool || auditLocalRoot
29
15
  ? async (sessionId) => {
@@ -37,24 +23,6 @@ export function createSessionFaces(ctx) {
37
23
  return { ...audit, owner: (await ownerAware.ownerOf?.(sessionId)) ?? null };
38
24
  }
39
25
  : undefined;
40
- // §0.5 E21 — DELETE /v1/sessions/:id purge coordinator. Deletes the session abstraction's CONVERSATION HISTORY
41
- // (sessionStorage.deleteSession → session_meta/session_event) PLUS the service-owned operational rows for that
42
- // session: the runs ledger (runStore.deleteBySession → task_run/task_active/task_event, session_id-scoped), the
43
- // durable checkpoints + resume-ctx (checkpointStore.deleteBySession, present only under DURABLE_APPROVAL), and
44
- // offloaded tool results (toolResultStore.deleteBySession). Wired only when the session store can actually
45
- // delete (a DB backend).
46
- //
47
- // 🔴 ORDERING (adversarial-review HIGH — privacy/right-to-delete + silent-success): the session HISTORY delete
48
- // (session_meta) is the `ownerOf` gate the DELETE route consults to decide idempotency. It MUST be the LAST
49
- // committed leg. If a privacy-relevant child leg (runs/checkpoint/tool-result) is deleted AFTER the meta row is
50
- // gone and then FAILS, the idempotent retry sees `ownerOf===undefined` (history already gone) → the route
51
- // short-circuits to 200 {deleted:false} and NEVER re-purges → those child rows are stranded while the API
52
- // reports success. So we purge the child legs FIRST and PROPAGATE any error (do NOT swallow): on a child
53
- // failure the history delete is SKIPPED, `session_meta` survives, the DELETE returns 500, and the idempotent
54
- // retry's `ownerOf` still resolves → it re-runs the FULL purge to convergence. `deleted` is computed from the
55
- // FINAL committed result (history-deleted || runsRemoved>0 — the privacy-relevant data that actually existed).
56
- // [1196] session-watch registry(SSE 订阅共享探针面)。谓词与 /head 探针同门(getLeafId+ownerOf);
57
- // env 旋钮经 posIntEnv 有界校验;getLeafId 箭头包一层=天然绑定宿主(SESSION_CACHE_TTL_SEC=0 裸类实例)。
58
26
  const sessionWatchRegistry = ownerAware.getHead && ownerAware.ownerOf
59
27
  ? new SessionWatchRegistry(async (sid) => (await ownerAware.getHead(sid)) ?? { owner: undefined, leafId: null }, {
60
28
  hotMs: posIntEnv(process.env.SESSION_EVENTS_HOT_MS, 200),
@@ -62,73 +30,21 @@ export function createSessionFaces(ctx) {
62
30
  maxWatchedSessions: posIntEnv(process.env.SESSION_EVENTS_MAX_PROBES, 512, 100_000),
63
31
  })
64
32
  : undefined;
65
- // S2 fast path([1208]③ 兑现):同副本 append 落点(tidb/pg persist post-commit)经 leaf-bus 直推
66
- // notifyLocal——单副本部署零延迟;owner 随 bus 传(commit-time row owner,session-leaf-bus 头注契约:
67
- // "Owner IS carried"),notifyLocal 据此做 generation 围栏(旧 A 世代不会假匹配新租户的叶子)。
68
- // local lane 无写钩,探针道照旧。
69
33
  setLeafAdvanceListener(sessionWatchRegistry ? (sid, leaf, owner, seq) => sessionWatchRegistry.notifyLocal(sid, leaf, owner, seq) : undefined);
70
34
  const purgeSession = ownerAware.deleteSession
71
35
  ? async (sessionId, owner) => {
72
- // Runs-ledger FIRST: it transactionally re-asserts the no-active-run invariant (FOR UPDATE on task_active,
73
- // serializing against a concurrent createRun). If a run claimed the session in the route's check→purge
74
- // window it returns `{ active }` and deletes nothing → we bubble it up so the route 409s, BEFORE touching
75
- // any other table (no partial purge of a live session). Each leg propagates its error so a child failure
76
- // aborts the purge BEFORE the history-delete gate is removed (keeping the idempotent retry convergent).
77
- // 🔴 run 账本按 session_id 单键删,**没有**行级属主门(F-A 修,2026-08-17 合并码扫描):`task_run.owner`
78
- // 是每次提交的 principal,与会话的 canonical owner 会分叉(无主存量会话被带 principal 的壳续聊 ⇒ 新行
79
- // owner="default",而这里递的是会话行的 owner=null)。行级门于是只删一半、把具名行连同 task_event 留成
80
- // 孤儿(session_meta 已删 ⇒ 留存腿的 sessionCandidates 永远枚举不到它们),路由却回 {deleted:true} ——
81
- // 与下面 anchors 腿注里那句 "a right-to-delete violation reported as success" 是同一个病,理由也同一条:
82
- // 会话所有权已在路由的 DELETE 门证过。
83
- // 🔴 **化身围栏**(codex R1-[high] 验真后加):属主门下来了,「路由验过权」这句话就必须还**成立到此刻**。
84
- // 会话 id 由调用方自选、删除后可被别人 `register` 重新登记 ⇒ 两只并发 purge 交错时(A 与 A' 同时过门,
85
- // A' 跑完整条 purge、租户 B 用同一 uuid 重登记并跑完一次 run),慢的那只会按 session_id 把 **B 的**行删掉。
86
- // 复读一次 canonical owner,不齐即整条中止(一行不删、回 {deleted:false} —— 属于调用方的那个化身已经不在)。
87
- // 这一读是**所有后端**都有的那道围栏;SQL 双生在 store 事务里还有一道 `session_meta … FOR UPDATE` 的
88
- // 原子复核(那才是真正消灭竞态的一道),local 车道靠它 + 数据根独占 BootLock。
89
- // 「行不在」(undefined)与「换了主」一律中止(codex R2 采纳):meta 不在 = 调用方验过权的那个化身
90
- // 已经没了,再按 session_id 往下删,删到的只可能是**别人**重新登记后写的行。E21 的次序契约保证
91
- // meta 是**最后**删的(子腿失败就不删 meta,让幂等重试收敛)⇒ 「meta 不在却还有本会话的孤儿子行」
92
- // 在协调器这条路上构造不出来,中止不会丢掉任何该删的东西。
93
- // ⚠️ `ownerOf` **方法缺席**的后端(可选 seam)与「行不在」必须分开判:合起来会让那些后端的删除
94
- // 整条恒中止(fail-closed 到把功能关掉),那是另一种缺陷。
95
36
  if (ownerAware.ownerOf) {
96
37
  const stillOwner = await ownerAware.ownerOf(sessionId);
97
38
  if (stillOwner !== owner)
98
39
  return { deleted: false };
99
40
  }
100
- // 📌 **残余,如实登记**(codex R2 判真但**射程外**,不在本批修):本围栏钉住的是「purge 开始那一刻
101
- // 化身是对的」+ run 腿的事务内原子复核。其余按 session_id 单键删的腿(anchors / exemption / policy /
102
- // attachment / snapshot / task_list(#318 新入册,codex R2-[high] 同判)/ tool_result 的 provenance 形)
103
- // 在**本批之前就是**这个 scoping(理由逐字见下方
104
- // anchors 腿注,可追到公开快照那一版),它们各自开始的时刻理论上仍可能落在一次化身漂移之后。
105
- // 要真正消灭它得让**整条**级联化身原子(SQL 单事务横跨十余张表 + 对象存储,即留存腿那个形),而在线
106
- // 删除面刻意不是那个形:它要跨事务崩溃收敛(meta 最后删 ⇒ 幂等重试),还要动 MinIO 字节。那是设计级
107
- // 改动(属主裁决 + 三问),不是复审批能顺手做的事;已作后续件登记,别把这条注读成「已解」。
108
41
  const runs = runStore ? await runStore.deleteBySession(sessionId, owner) : { removed: 0 };
109
42
  if ("active" in runs)
110
43
  return { active: runs.active };
111
- // checkpoint rows carry no owner column → owner-guarded via a session_meta.owner sub-select, so they MUST
112
- // run while session_meta still exists (before the history delete below); tool_result selects by its
113
- // RECORDED PROVENANCE column (`owner_session_id`) and stays route-guarded too (see its deleteBySession doc).
114
44
  if (checkpointStore)
115
45
  await checkpointStore.deleteBySession(sessionId, owner);
116
- // optional extra(R3 复扫更正:不再是「SQL twins only」——core 5.28 起 FileToolResultStore **实现了**
117
- // deleteBySession,local 车道这条腿**真的会跑**。File 孪生的形:同步遍历目录、无 .owner.json 边车的
118
- // 存量文件计入 unattributable 不删——所以 local 部署上这条 warn 在有 5.28 前存量时会以大数出现,
119
- // 这是诚实读数不是事故。⚠️ R4 复扫再纠:**local 无 TTL 腿**(File 孪生没有 reapOlderThan,
120
- // reapers 的可选链在 local 恒 no-op)⇒ 这些无主存量今天在 local 上**没有任何清理路径**
121
- // (会话删除按契约不动它、TTL 也够不着)——只占磁盘不泄权,单用户 CC 姿态下按已知缺口登记,
122
- // 清理腿(File 孪生补 reapOlderThan 或一次性迁移铸边车)是独立后续件,别把本注读成已有补偿。)
123
46
  if (toolResultStore) {
124
47
  const report = await toolResultStore.deleteBySession?.(sessionId);
125
- // 🔴 **回执不许丢**(codex 复审 R1-[high],验真后修):core 把 `unattributable` 写成「the caller
126
- // learns the deletion was **incomplete** rather than being told a clean "done"」——无出处的行按
127
- // 契约永不删除,协调器把报告丢掉就等于把这句话吞了,而删除面吞掉的是**删除权**的真相。
128
- // 这一格是**全库**无出处行数(core InMemory 孪生口径),读法 = 「本店存在没人认领的字节,任何
129
- // 会话删除都不可能完整」,不是「本会话残留了 N 行」;实体清理归 TTL(`reapOlderThan`)。
130
- // ⚠️ 登记后续件:把它也带上 DELETE 的 HTTP 回执是**加 wire 键**,归属主裁决,不在本批自作主张;
131
- // 本批先保证它不再无声消失(结构化 warn = 运维查得到、审计追得上)。
132
48
  if (report !== undefined && report.unattributable > 0) {
133
49
  logger.warn("tool_result_unattributable_rows_survive_session_delete", {
134
50
  sessionId,
@@ -137,60 +53,21 @@ export function createSessionFaces(ctx) {
137
53
  });
138
54
  }
139
55
  }
140
- // E18 resume-at anchors are per-session privacy-relevant metadata → purge ALL of them with the session, scoped
141
- // by session_id ALONE (the route already proved session ownership at the DELETE gate). A per-ROW owner guard
142
- // here would LEAK: an anchor's owner is the per-run submitting principal, which diverges from the canonical
143
- // session owner when an anonymous session is later attached by a principal (REQUIRE_PRINCIPAL=false) — those
144
- // mixed-owner rows would survive the delete (a right-to-delete violation reported as success).
145
56
  if (resumeAnchorStore)
146
57
  await resumeAnchorStore.deleteBySession(sessionId);
147
- // Approval exemptions are per-session operator decisions → purge with the session (same
148
- // session_id-alone scoping rationale as the anchors above).
149
58
  if (approvalExemptionStore)
150
59
  await approvalExemptionStore.deleteBySession(sessionId);
151
- // E6 session-policy rows are per-session operator rules → purge them too, scoped by session_id (the route
152
- // owner-gated the session). Since core 1.423 the seam is on the INTERFACE (optional) and core's File/InMemory
153
- // stores carry it too — so this fires on EVERY backend now. (The old note here claimed local rules were
154
- // "process-ephemeral anyway" — false for the file-backed local store, whose policy rows survive restarts;
155
- // that right-to-delete hole is what [1796]§三 → core 1.423 closed.)
156
60
  if (sessionPolicyStore?.deleteBySession)
157
61
  await sessionPolicyStore.deleteBySession(sessionId);
158
- // D-1 附件随会话删(E21 级联;session_id 单键 scoping,与 anchors 同理由——路由已证会话所有权)。
159
62
  if (taskAttachmentStore)
160
63
  await taskAttachmentStore.deleteBySession(sessionId);
161
- // #318 任务清单(TaskCreate/TaskList 家族)是 per-session 的用户内容(任务标题/描述/元数据)
162
- // → 随会话删,scoping 与 anchors 同理由(路由已在 DELETE 门证过会话所有权)。
163
- // 🔴 **不是可选的卫生腿**:会话 id 由调用方自选、删除后可被别人 `register` 重新登记,而车道的
164
- // listKey 是 sessionId 的确定性派生 ⇒ 不删的话新化身第一次 TaskList 就读到上一位主人的任务,
165
- // TaskCreate 还从上一位的高水位续号(codex R1-[high] 验真后接)。错误如实上抛 = 与同族腿一致
166
- // (子腿失败 ⇒ 不删 meta ⇒ 幂等重试收敛)。
167
64
  if (taskListLane)
168
65
  await taskListLane.deleteBySession(sessionId);
169
- // E19 rewind-files snapshots are per-session working-tree state → purge them too (scoped by sessionId; the
170
- // durable twins carry deleteBySession, local InMemory omits it → optional no-op on local).
171
66
  if (fileSnapshotStore?.deleteBySession)
172
67
  await fileSnapshotStore.deleteBySession(sessionId);
173
- // P1 ①②: drop any pending async-workflow completions for this session (else they orphan when a later
174
- // tenant reclaims the sessionId). Best-effort + non-fatal — a leftover entry is ALSO gated by the
175
- // drain-time owner check, so a purge failure never leaks; it must not abort the session delete.
176
- // `owner` scopes the purge fence (1.80): only the DELETED session's own late completions are fenced —
177
- // a new tenant legitimately re-claiming this sessionId within the window keeps its push (review MED).
178
68
  if (workflowCompletionInbox)
179
69
  await workflowCompletionInbox.purge(sessionId, owner ?? null).catch((err) => logger.warn("workflow_completion_inbox_purge_failed", { sessionId, err: String(err) }));
180
- // 修8(三路复审 absorb-2,接线 (a)):the session's scratchpad dir ([820]③, ensureScratchpadDir) rides
181
- // the session lifecycle — purge it with the session. purgeScratchpadDir SELF-SWALLOWS (env-facts.ts
182
- // contract: cleanup never faults the delete path; the periodic sweep below is the convergence backstop),
183
- // so it cannot strand the E21 ordering invariant. Same root the ensure used (config.localDataRoot ?? localRoot).
184
70
  await purgeScratchpadDir(config.localDataRoot ?? localRoot, sessionId);
185
- // design/129 (core 1.239): reap this session's SESSION-scoped background children — they
186
- // deliberately outlive turns (backgroundScope:"session"), so the session's DELETE is their lifecycle
187
- // end (core's parent-teardown reap skips them by design; without this they run to their forced timeout).
188
- // Sync + replica-local (the registry is in-process); best-effort — a reap of 0 on the wrong replica is
189
- // covered by core's forced child-timeout cap.
190
- // [1892]{core} 跨仓待办:scope 轴必传——core canAccess 对缺省 scope fail-closed(拒绝非通配,
191
- // 1.441 复审①正过极性),不传 = 四类后台任务在会话删除时恒 0 回收(潜伏至今,core 复审顺手
192
- // 核出)。铸值同源 runs.ts:25 契约:core 按 `spec.principal ?? "default"` 注册 ⇒ 此处
193
- // owner(会话主)?? "default" 对齐;错 scope 不越租户由 canAccess 保证(session-reap-scope 钉)。
194
71
  try {
195
72
  const reaped = defaultTaskRegistry.reapSessionBackground(sessionId, owner ?? "default");
196
73
  if (reaped > 0)
@@ -199,20 +76,12 @@ export function createSessionFaces(ctx) {
199
76
  catch (err) {
200
77
  logger.warn("session_background_reap_failed", { sessionId, err: String(err) });
201
78
  }
202
- // History/meta LAST: once committed, the `ownerOf` gate is gone and the delete is durably complete. The
203
- // owner is threaded into the SQL guard (session_meta.owner) for data-layer defense-in-depth.
204
79
  const historyDeleted = await ownerAware.deleteSession(sessionId, owner);
205
- // [1196] 三轮复审(租户轮转围栏,同副本半场):session 删除即终结其 SSE 订阅者+watch 条目——
206
- // 旧 owner 的活流不得跨越 delete/reclaim 边界收新租户的 head;跨副本半场=路由心跳期 owner 复核。
207
80
  sessionWatchRegistry?.dropSession(sessionId);
208
81
  return { deleted: historyDeleted || runs.removed > 0 };
209
82
  }
210
83
  : undefined;
211
- // Degenerate-output a/b instrument: needs the durable session log to inspect turn history (design/39
212
- // ② gating data). Only on the TiDB backend (mysql2 pool); env-only / PG deploys get no-op.
213
84
  const instrumentDegenerate = mysqlPool ? makeDegenerateInstrument(mysqlPool, metrics, logger) : undefined;
214
- // Plan-cache recurrence probe (core design/42, INSTRUMENT-FIRST): pure in-memory, no backend needed —
215
- // counts per-scope task-objective recurrence so core can decide whether to build plan caching.
216
85
  const planCacheProbe = new PlanCacheProbe(metrics, logger);
217
86
  return { ownerAware, sessionAudit, sessionWatchRegistry, purgeSession, instrumentDegenerate, planCacheProbe };
218
87
  }
@@ -1,32 +1,9 @@
1
- /**
2
- * design/158 A10:composition root 分段 —— 进程收尾(hardShutdown / drain / 三个信号处理器)。
3
- *
4
- * 纯搬运:函数体逐字来自 `main.ts`(原 4385-4488 行),缩进不变;新增的只有 import 与
5
- * `installShutdownHandlers(ctx)` 包壳。
6
- *
7
- * ⚠️ **位置即契约**(搬运不得改次序):
8
- * 1. 本段必须在 `server.listen()` **之后**调用 —— `process.on("SIGTERM"|"SIGINT"|"SIGHUP")` 的注册
9
- * 时点即"从此刻起信号可被优雅处理";提前注册会让一个尚未 listen 的进程走 drain 分支。
10
- * 2. 三个 `process.on` 的**相对次序**保持 SIGTERM → SIGINT → SIGHUP(与原文件逐字一致);
11
- * 它们互不覆盖,但 handler 之间共享 `closing`/`draining` 两个 latch,次序变化会改变
12
- * "第二个信号"的语义读数。
13
- * 3. `clearInterval(reaper)` 必须是 hardShutdown 的第一件事(在 `server.close()` 之前),否则
14
- * 收尾期还会有 reaper tick 打向正在关闭的池。
15
- */
16
1
  import { Runner, defaultTaskRegistry } from "@sema-agent/core";
17
2
  import { writeCrashLast, clearCrashLast, readCrashLast } from "./crash-last.js";
18
3
  import { createSighupIdleHandler } from "../sighup-idle.js";
19
4
  import { createParentWatch } from "../parent-watch.js";
20
- /** 注册 SIGTERM/SIGINT/SIGHUP 收尾链。**必须在 listen 之后调用**(见文件头「位置即契约」)。 */
21
5
  export function installShutdownHandlers(ctx) {
22
6
  const { config, logger, server, reaper, fleetReconcile, retentionLane, releaseRetentionLease, otelExporter, breakerState, costQuota, rateLimiter, runner, subRunner, lspManager, workflowNotifyJournal, fleetClient, backend, drainState, storeLiveProbe, configCenter, version, } = ctx;
23
- // #320①([4728] 五点令)—— 崩溃遗言面。契约:`crash-last.json` 在场 ⇔ 上一次退出=崩溃。
24
- // · boot 提示:上次崩溃留了遗言 ⇒ 一行 warn(读回即证据,不清——清的责任归受控退);
25
- // · uncaught 双钩:写遗言(version/pid/uptime/inflight/err 首行消毒)后仍按崩溃语义 exit(1),不吞崩溃;
26
- // · 受控退:hardShutdown 一进来即 clearCrashLast(这次是有意的退)——若后续收尾半途真崩,
27
- // uncaught 钩会重写遗言,判别不失真。
28
- // dataRoot 类型上可选(config 解析恒回退 ~/.ai-agent ⇒ 实际恒真,/health dataRoot 同注);类型面
29
- // 诚实处理:真缺席=遗言面整体不装(uncaught 钩仍装——崩溃语义不能因遗言面缺席而变)。
30
7
  const dataRoot = config.localDataRoot;
31
8
  if (dataRoot) {
32
9
  const prior = readCrashLast(dataRoot);
@@ -39,7 +16,6 @@ export function installShutdownHandlers(ctx) {
39
16
  };
40
17
  process.on("uncaughtException", (err) => {
41
18
  writeLastWords(err);
42
- // 与 Node 缺省崩溃姿势对齐:stack 打 stderr(遗言只留首行),随后按崩溃码退。
43
19
  console.error(err);
44
20
  process.exit(1);
45
21
  });
@@ -49,25 +25,15 @@ export function installShutdownHandlers(ctx) {
49
25
  process.exit(1);
50
26
  });
51
27
  let closing = false;
52
- /** #219:parent 监视腿(装配在本函数尾,`config.parentPid` 在场才建)。声明提前只为让 hardShutdown
53
- * 能把它一并停掉 —— 文件头契约 3 同族(收尾期不再有后台 tick)。 */
54
28
  let parentWatch;
55
29
  const hardShutdown = () => {
56
30
  if (closing)
57
31
  return;
58
32
  closing = true;
59
33
  if (dataRoot)
60
- clearCrashLast(dataRoot); // #320①:受控退不留遗言(两形可判别;半途真崩会被 uncaught 钩重写)
34
+ clearCrashLast(dataRoot);
61
35
  clearInterval(reaper);
62
- clearInterval(fleetReconcile.timer); // #261:契约 3 同族——收尾期不再有对账 tick 打向正在关闭的池
63
- // #270 车2:契约 3 同族,且这一条最要紧——留存 tick 起飞的是跨十余张表的**破坏性**事务。
64
- // 🔴 **四步的次序都是承重的**(codex 对抗复审 R1/R2 两轮各修一半,均验真后改):
65
- // 停表 → 等在飞那一轮落地 → 让租 → `backend.close()`。
66
- // · `clearInterval` 只挡新 tick,不等当前这一轮;停表之后紧跟着就让租 ⇒ 新副本立刻接租,而本副本
67
- // 还在删同一个域的数据 —— 一个由**正常** SIGTERM/SIGINT 稳定触发的双执行者窗口(R1);
68
- // · 而让租之后若不等就关池,这一轮会被从底下抽掉连接、半途而废(R2 —— 见下方 server.close 回调)。
69
- // · 进程若在整条链跑完之前就退了,租约不会被让出 ⇒ 下一位等满 TTL —— 那是**安全**的那一侧。
70
- // 三步的产物是一条**可等的** promise —— 下面 `server.close()` 的回调必须等它落地才 `backend.close()`。
36
+ clearInterval(fleetReconcile.timer);
71
37
  const retentionTeardown = retentionLane === undefined
72
38
  ? Promise.resolve()
73
39
  : retentionLane
@@ -75,27 +41,20 @@ export function installShutdownHandlers(ctx) {
75
41
  .then(() => releaseRetentionLease?.())
76
42
  .then(() => undefined)
77
43
  .catch(() => undefined);
78
- fleetReconcile.reconciler.stop(); // 摘总线订阅(观察者已停,扇出不该再喂它)
79
- parentWatch?.stop(); // #219:契约 3 同族——收尾期不再探父进程
80
- storeLiveProbe?.stop(); // #131-2:契约 3 同族——收尾期不再有探针 tick 打向正在关闭的池
81
- configCenter?.stopRefreshLoop(); // #131-2:停机中途不再热应用配置
44
+ fleetReconcile.reconciler.stop();
45
+ parentWatch?.stop();
46
+ storeLiveProbe?.stop();
47
+ configCenter?.stopRefreshLoop();
82
48
  otelExporter?.stop();
83
49
  breakerState?.stop();
84
50
  if (costQuota && "stop" in costQuota)
85
- costQuota.stop(); // the DB-backed quota/limiter (TiDB|PG) have a refresh loop to stop; the in-memory ones don't
51
+ costQuota.stop();
86
52
  if (rateLimiter && "stop" in rateLimiter)
87
53
  rateLimiter.stop();
88
54
  runner.sessions.dispose();
89
55
  subRunner.sessions.dispose();
90
- // TOC local LSP: NodeLspManager holds LOCAL child-process language servers — kill them on shutdown so a
91
- // restart doesn't orphan a fleet of stdio servers. dispose() isn't on the LspServerManager interface (only the
92
- // concrete managers have it), so probe it; the e2b/k8s bridge manager is a harmless no-op if it lacks one.
93
56
  void lspManager?.dispose?.();
94
- // SVC-1: release the File journal's append handle; the SQL twins hold no fd (probe — close isn't on the seam).
95
57
  void workflowNotifyJournal?.close?.();
96
- // core 1.270.1:hardShutdown 终点(drain 已完,in-flight 不再被破坏)全量收割 session 驻留
97
- // bg bash(detached 进程组,引擎死后 reparent PID 1 残留=实测;retain-declared 例外在原语内)。
98
- // TOB 容器形态同样调=无害且显式;SIGKILL 路径无钩=已知不可救记档。
99
58
  try {
100
59
  const reaped = defaultTaskRegistry.reapAllSessionBackground();
101
60
  if (reaped > 0)
@@ -104,34 +63,16 @@ export function installShutdownHandlers(ctx) {
104
63
  catch (err) {
105
64
  logger.warn("shutdown_background_reap_failed", { err: String(err) });
106
65
  }
107
- void fleetClient?.stop(); // stop heartbeat + flush final usage window + best-effort DELETE deregistration
66
+ void fleetClient?.stop();
108
67
  server.close(() => {
109
- // 🔴 `backend.close()` 必须排在留存收尾**之后**(codex 对抗复审 R2-[high],验真后修):
110
- // 那一轮打的是跨十余张表的破坏性事务,把连接池从它底下抽掉 = 这一轮自己半途而废
111
- // (比"让租早了"更重:让租早了只是**可能**双执行者,池被抽掉是必然的半成品)。
112
- // 兜底仍在:下面那条 15s `finalExit` 无条件退出,一轮卡死的 sweep 拖不住停机。
113
68
  void retentionTeardown.then(() => (backend ? backend.close() : Promise.resolve())).finally(() => process.exit(0));
114
69
  });
115
- // 对抗复查 B-4(HIGH):the exit above only fires after backend.close() SETTLES — a wedged pool teardown
116
- // (dead DB, hung socket) kept the "force-terminated" process alive indefinitely (the 10s closeAllConnections
117
- // below unblocks server.close(), but nothing bounded backend.close). FINAL deadline: whatever is still
118
- // holding after 15s, exit anyway (durable state is already flushed by then — checkpoint/terminal writes
119
- // happen before hardShutdown; the pool teardown is best-effort cleanup, not correctness).
120
70
  const finalExit = setTimeout(() => { logger.warn("hard_shutdown_final_deadline", { afterMs: 15_000 }); process.exit(0); }, 15_000);
121
71
  finalExit.unref?.();
122
- // `server.close()` waits for every connection to end — an open SSE stream (/v1/tasks/stream, /events) would
123
- // otherwise block it forever, so pool.end()/exit never run and the process hangs until SIGKILL. Drop idle
124
- // keep-alives now, then force-terminate any lingering connections after a grace period so `close()` can
125
- // resolve and shutdown completes. The timer is unref'd but still fires while the server holds connections.
126
72
  server.closeIdleConnections?.();
127
73
  const forceClose = setTimeout(() => server.closeAllConnections?.(), 10_000);
128
74
  forceClose.unref?.();
129
75
  };
130
- // SIGTERM = graceful DRAIN — flip `draining` (new billable submits 503+Retry-After, /health carries
131
- // draining:true for readiness摘流), then wait for this instance's in-flight legs (live streams + bg/resume) to
132
- // finish before the hard shutdown. Bounded by DRAIN_GRACE_MS (default 10min — an interactive turn is minutes;
133
- // the previous behavior was a 10s hard-cut that killed long turns). A SECOND SIGTERM or SIGINT (dev Ctrl-C)
134
- // skips the wait — k8s sends SIGKILL after terminationGracePeriodSeconds regardless, so the escape hatch is free.
135
76
  let draining = false;
136
77
  const drainThenShutdown = () => {
137
78
  if (closing)
@@ -144,17 +85,14 @@ export function installShutdownHandlers(ctx) {
144
85
  draining = true;
145
86
  drainState.draining = true;
146
87
  drainState.since = Date.now();
147
- void fleetClient?.announceNow(); // draining 翻转即刻再 announce — 第一时间自摘流,比任何观测都快
88
+ void fleetClient?.announceNow();
148
89
  const inflight = drainState.inflight?.() ?? 0;
149
90
  logger.info("draining_started", { inflight, graceMs: config.drainGraceMs });
150
91
  if (inflight === 0) {
151
92
  hardShutdown();
152
93
  return;
153
94
  }
154
- // 修3(三路复审 B3-2 时钟):drain elapsed/截止计算走 performance.now() 单调域(挂钟跳变不得吞掉/拉长
155
- // drainGraceMs 窗);对外报告的 drainState.since(/health drainingSince)保留 epoch(Date.now)。
156
95
  const t0 = performance.now();
157
- // NOT unref'd on purpose — this timer IS the shutdown driver (the open connections keep the loop alive anyway).
158
96
  const tick = setInterval(() => {
159
97
  const n = drainState.inflight?.() ?? 0;
160
98
  const elapsed = Math.round(performance.now() - t0);
@@ -167,16 +105,8 @@ export function installShutdownHandlers(ctx) {
167
105
  };
168
106
  process.on("SIGTERM", drainThenShutdown);
169
107
  process.on("SIGINT", hardShutdown);
170
- // SIGHUP(壳窗口关闭把信号打到整个进程组)≠「没人在用这只引擎」:共享引擎形态(同 config 多壳会话,
171
- // 引擎按 T11 防孤儿拍骑在首壳进程组里)下 peer 会话还活着——无条件 drain 会把 peer mid-turn 斩掉(499)
172
- // 且窗内新提交 503(B3,壳侧 [771]② 取证)。改 hup-pending 空闲窗:不翻 draining(peer 零感知),
173
- // inflight()(bg/resume legs + live streams)连续 sighupIdleGraceMs 为 0 才 hardShutdown,任何在飞 leg
174
- // 重置窗口。真孤儿(最后一窗关掉)有界自灭=T11 保留;第二个 SIGHUP 升级走既有 drain 路径(硬梯子)。
175
- // session 驻留 bash 的全量收割仍挂 hardShutdown 终点(reapAllSessionBackground)。
176
108
  process.on("SIGHUP", createSighupIdleHandler({
177
109
  inflight: () => drainState.inflight?.() ?? 0,
178
- // ⚠️ 时基契约(三路复审修3):lastActivityAt 由 server.ts 以 performance.now() 单调域打点,与
179
- // sighup-idle.ts 内部的 `now` seam(缺省 performance.now)同域——不得混入 Date.now 值。
180
110
  lastActivityAt: () => drainState.lastActivityAt?.() ?? 0,
181
111
  isStopped: () => closing || draining,
182
112
  escalate: drainThenShutdown,
@@ -184,10 +114,6 @@ export function installShutdownHandlers(ctx) {
184
114
  idleGraceMs: config.sighupIdleGraceMs,
185
115
  log: (event, fields) => logger.info(event, fields),
186
116
  }));
187
- // #219(黑板 [3617]):壳被 SIGKILL 后引擎被 reparent 到 init 而继续活着(孤儿引擎持端口/持库连接,
188
- // 且再没有人会给它发 SIGTERM)。`SEMA_PARENT_PID` 设了才装配 —— 连续两拍 ESRCH 判死,出口**就是**
189
- // 上面 SIGTERM 用的这条 `drainThenShutdown`(结算/park 语义与人工停机逐字一致,不是裸 exit)。
190
- // 位置:与三个 process.on 同段、在它们之后 —— 它和信号腿共享 closing/draining 两个 latch。
191
117
  if (config.parentPid !== undefined) {
192
118
  parentWatch = createParentWatch({
193
119
  pid: config.parentPid,
@@ -1,119 +1,17 @@
1
- /**
2
- * #303([4610] test 报「/model 中途切换校验探针经 POST /v1/side-query 对有效 key 返回上游 401」)——
3
- * side-query 面的 **per-model key plane** 装配。
4
- *
5
- * ## 病灶(#303 立案时亲验 core 5.43.0 dist 实现行)
6
- *
7
- * core 的 side-query verb 当时**结构性没有 key 座位**:
8
- * · `core/side-query.d.ts` 的 `SideQuerySpec` 无 `apiKey` / `getApiKeyAndHeaders` 字段;
9
- * · `SideQueryDeps` = `{ brain, models?, roles? }`;
10
- * · `core/side-query.js` 铸 brain `options` 时只放 `signal/maxTokens/reasoning`,**不含 apiKey**。
11
- * 而 openai brain 的 `buildRequest`(`brain/openai.js`)是这两行:
12
- * `const apiKey = options?.apiKey ?? config.apiKey;` ← 缺席即回落 **brain 构造时的网关 key**
13
- * `const root = (model.baseUrl || config.baseUrl || "")` ← 却**按模型**选路
14
- * ⇒ 对带 per-model `baseUrl` + `apiKeyEnv`/`sealedApiKey` 的模型,side-query 把**主网关 key 发到
15
- * 外部模型 URL**:用户看到上游 401,同时这是一次凭据外泄(安全轴)。
16
- * 当时的窄修 = 一只 **key 注入 brain wrapper**(`createPerModelKeyBrain`),并在顶注写明「core 一旦
17
- * 给 `SideQuerySpec` 补上 key 座位,本 wrapper 即可撤」,撤除信号挂机器钉。
18
- *
19
- * ## #341 换装(core 5.46.0 提货,[4615] 座位到货 —— 撤除条件已满足)
20
- *
21
- * core 5.46.0 的 `SideQuerySpec` 有了 `getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"]`,
22
- * `runSideQuery` 每次调用按**解析后的** `Model` 调它,并把 `{apiKey, headers}` 铸进 brain options
23
- * (`dist/core/side-query.js`:`const auth = await spec.getApiKeyAndHeaders?.(resolved.model);`)——
24
- * 与主推理链逐字同一只座位形。于是 side-query 席**撤掉 brain wrapper**,key 供给改走 spec 座位:
25
- * · 本席不再在 side-query 路上包 `ctx.brain`,`RunnerDeps.brain` 的零扰动从「wrapper 只包一只引用」
26
- * 降级成**结构性事实**(这条路上根本没有 wrapper);
27
- * · `headers` 半边从此有出路(wrapper 形只注得进 `apiKey`,core 的座位 `{apiKey, headers?}` 两半都收);
28
- * · 判据(`gatewayBaseUrl`)与 resolver 一样**按调用现取** —— 见下面 `createSideQueryLane` 的注。
29
- * `createPerModelKeyBrain` **不删**:它仍是 WebFetch 摘要面(`boot/webfetch-summarize-lane.ts`)的
30
- * key plane —— core 的 `WebFetchSummarizerOptions` 至今只有 `maxContentChars`,那一面没有座位可换
31
- * (亲验 core 5.46.0 `dist/tools/web.d.ts`)。
32
- *
33
- * 🔴 **poison(sealed key 不可解)上抛,绝不静默回落网关 key** —— `resolveModelApiKey` 抛
34
- * `SealedKeyPoisonedError` 是「这个模型配了托管密钥但解不开」的响亮态;吞成 undefined 等于用共享
35
- * 网关账号去打这只模型的上游,正是毒丸机制存在的理由(CLAUDE.md #157 安全轴 fail-closed)。换装后
36
- * 这条语义由 core 承载得更彻底:座位抛 ⇒ `runSideQuery` 的 `await` 直接上抛,brain 一个字节都没发。
37
- *
38
- * ## 具名残余:**无 per-model key 的 off-route 模型仍收网关 key**(codex R1-F1,逐条评估后如实留)
39
- *
40
- * 判据本身没变(座位到货也没改它):`undefined` ⇒ 座位不给凭据 ⇒ core 用网关 key,即使这只模型的
41
- * `baseUrl` 指向别家。为什么**不**在本批把它改成 fail-closed(逐条,而不是"先不管"):
42
- * ① **与主推理链同语义**才是当前的正确性基准 —— 任务面(core 每次调 `getApiKeyAndHeaders`,返回
43
- * `undefined` 即网关 key)在**同一个** catalog、**同一批**模型上就是这个姿势。只把 side-query 收严,
44
- * 等于让同一只模型「跑任务能用、问一句就 401」,而任务面才是量大的那一面 —— 半边收严的覆盖比
45
- * 统一的宽松更难排障。
46
- * ② **谁被伤到**:`baseUrl` 是**运维**在 config-center 里自己写的(不是用户输入,用户只能在
47
- * `atModelAllowlist` 里点名单内的模型)。"同一账号多网关主机/多区域"是既有且正当的部署形,
48
- * 它今天靠网关 key 工作;无版本协商地收严 = 这类部署当场 401,而且没有旋钮可回退。
49
- * ③ hook 面(`hooks/hook-llm.ts`)确实是 fail-closed 的,但它的辖域是 **hook roster**(运维给钩子挑的
50
- * 模型),不是用户可点的整本目录;两者的收严代价不同量级。
51
- * ⇒ 本批**不改行为**,把它作为**跨面语义问题**上报(要收严就三面一起收严、带旋钮、走表态制),
52
- * 而不是在这一面偷偷收严。真要收严时的正解:按 `normUrl` 比对 `gatewayBaseUrl`(+fallback 列表)
53
- * 与 anthropic 路由,off-route ∧ 无 per-model key ⇒ 拒发,并同批改任务面。
54
- */
55
1
  import { expandTiers, runSideQuery } from "@sema-agent/core";
56
2
  import { recordFailOpen } from "../observability/fail-open.js";
57
- /**
58
- * 「这只模型的 baseUrl 打的是**别家**主机吗」—— core 的选路规则是 `model.baseUrl || config.baseUrl`,
59
- * 所以只有 `model.baseUrl` 非空时才可能离开本部署网关。比较按 URL 归一(`new URL` 小写 host、丢默认端口、
60
- * 去尾斜杠),裸字符串比会在 host 大小写/默认端口写法上假阴。
61
- *
62
- * 参照系缺席(`gatewayBaseUrl` 未给)⇒ 回 `false`:判不了就不判,别猜。
63
- *
64
- * ⚠️ `hooks/hook-llm.ts` 有一只逐字同形的内联归一器(它用同一判据做 **fail-closed 拒发**,本函数只
65
- * 做**计数**)。两处合一属跨面收严件 #309 的射程(那时三面同批换判据),本批刻意不动 hook 面的 fail-closed
66
- * 门 —— 在只改可观测性的批里重写另一条安全轴的门,风险与收益不对等。
67
- */
68
3
  export function isOffRouteBaseUrl(modelBaseUrl, gatewayBaseUrl) {
69
4
  if (!modelBaseUrl || !gatewayBaseUrl)
70
5
  return false;
71
- // 解析不了(配歪的 baseUrl)⇒ 退到去尾斜杠的裸串比。用 `URL.canParse` 而不是 try/catch:静默降级
72
- // 棘轮门(`test/silent-degradation-scan.test.ts` SHAPE C)对「catch 只 return 默认值」是零容忍的,
73
- // 而这条降级恰好有一个不吞异常的写法(Node ≥20 / ES2023 均在射程)。方向:比不出同一家 ⇒ 算 off-route
74
- // ⇒ 记债(宁可多记一条待查,不可把一次真外泄判成自家网关)。
75
6
  const norm = (u) => URL.canParse(u) ? ((x) => `${x.protocol}//${x.host}${x.pathname.replace(/\/+$/, "")}`)(new URL(u)) : u.replace(/\/+$/, "");
76
7
  return norm(modelBaseUrl) !== norm(gatewayBaseUrl);
77
8
  }
78
- /**
79
- * per-model auth 座(#341,core 5.46.0 `SideQuerySpec.getApiKeyAndHeaders` 的 server 侧填充物)。
80
- *
81
- * 语义(逐条对齐主推理链 `getApiKeyAndHeaders`,也逐条对齐它取代的 wrapper 那三条臂):
82
- * · `upstream`(spec 自带的座位)**解出凭据** ⇒ 原样透传,含 `headers`(上游/装饰器已经定了凭据,
83
- * 本层不抢 —— wrapper 时代的「`options.apiKey` 已在场」臂);判别按**每次调用**的返回值,不是
84
- * 「座位在场就整只让位」:上游对这只模型没意见时,本席照常按模型解 key;
85
- * · resolver 缺席(部署根本没配 per-model key)⇒ 回 `undefined` ⇒ core 用网关 key(今日行为逐字不变);
86
- * · resolver 返回 undefined(这只模型没有自己的 key)⇒ 回 `undefined` ⇒ 网关 key(additive 契约);
87
- * · resolver 抛(sealed key 中毒)⇒ **上抛**,`runSideQuery` 的 `await` 直接把它扔给调用方,
88
- * 本次调用整体失败,一个字节都不发。
89
- *
90
- * 🔴 A-057.59 [PARTIAL high](2026-08-19;#341 换装后**处置不变**)—— 「无 per-model key 的 off-route
91
- * 模型仍收网关 key」这条具名残余**行为不变**(逐条理由见模块顶注「具名残余」段;core 的座位是
92
- * 「absent ⇒ construction-time credentials apply」,与主推理链
93
- * `dist/engine/harness/agent-harness.js` 的 `auth?.apiKey !== undefined` 臂逐字同语义,座位到货并没有
94
- * 改变这条残余,所以 `P-DEBT` 计数**不可销**),但**每次调用零留痕**这一半是要清偿的存量欠账:
95
- * config-apply 期那条 `sema_registry_model_key_env_missing` warn 只覆盖「声明了 env 名却没设」,覆盖不到
96
- * 「压根没配 per-model key 的 off-route 模型」,而后者才是真发生外泄的那一形。按 CLAUDE.md #157
97
- * 「一时改不动的 P 类记 `P-DEBT`」补计数:命中条件 = **解不出 per-model key ∧ 该模型的 baseUrl 不是本
98
- * 部署网关** ⇒ 这一次调用真把网关凭据发给了别家主机。`gatewayBaseUrl` 缺席(调用方没给参照系)⇒ 不计
99
- * (不猜),与「宁可漏计不可错计」同向。
100
- */
101
9
  export function createPerModelAuthSeat(getKeyResolver, opts = {}) {
102
10
  return async (model) => {
103
- // 🔴 **同代配对**(codex R1-[high],验真后采纳):resolver 引用必须在**第一个 await 之前**取。
104
- // core 在同一个同步段里解析出 `model`(含它那一代的 `baseUrl`)后立刻调本席,所以本函数体的**第一条
105
- // 同步语句**与模型解析同代。`await opts.upstream?.(model)` 是一道真 await 边界(`opts.upstream`
106
- // 缺席时 `await undefined` 照样让出一个微任务),config-center 的 apply 提交段(#303 codex R1-F2 修成
107
- // 「目录与密钥表零 await 窗成对换代」)可以整段落在那个缝里 ⇒ 缝之后再取 resolver 就会拿到**下一代**
108
- // 的密钥表,配上**上一代**的模型/端点:新家的 key 发到旧家的 URL —— 正是本席存在要消灭的那个形。
109
- // 取引用是纯读、无副作用,即使上游随后胜出也不多花什么。
110
11
  const resolve = getKeyResolver();
111
12
  const given = await opts.upstream?.(model);
112
13
  if (given?.apiKey !== undefined)
113
- return given; // 已有凭据:不抢
114
- // 🔴 不 try/catch:poison 必须上抛(静默回落 = 拿共享网关账号打这只模型的上游)。
115
- // resolver 缺席(部署根本没配 per-model key 平面)与解析回 undefined(这只模型没有自己的 key)
116
- // 两形都落到同一条缺席语义:回 undefined ⇒ core 用网关 key(additive 契约)。
14
+ return given;
117
15
  const resolved = resolve ? await resolve({ name: model.name }) : undefined;
118
16
  if (resolved === undefined && isOffRouteBaseUrl(model.baseUrl, opts.gatewayBaseUrl)) {
119
17
  recordFailOpen("server.model-key.off-route-model-uses-gateway-key", model.name);
@@ -121,34 +19,17 @@ export function createPerModelAuthSeat(getKeyResolver, opts = {}) {
121
19
  return resolved;
122
20
  };
123
21
  }
124
- /**
125
- * per-model key 注入 brain wrapper —— **#341 后只服务 WebFetch 摘要面**
126
- * (`boot/webfetch-summarize-lane.ts`)。side-query 面已换到 core 的 spec 座位(见顶注「#341 换装」);
127
- * core 的 `WebFetchSummarizerOptions` 至今只有 `maxContentChars`(亲验 5.46.0 `dist/tools/web.d.ts`),
128
- * 那一面**没有座位可换**,只能继续在 brain 层补凭据。
129
- *
130
- * 语义(与 `createPerModelAuthSeat` 同一张表,只是注入位在 `options.apiKey`):
131
- * · `options.apiKey` **已在场** ⇒ 原样透传(上游/装饰器已经定了凭据,本层不抢);
132
- * · resolver 缺席 / 返回 undefined ⇒ 不注入 ⇒ 网关 key(additive 契约);
133
- * · resolver 抛(sealed key 中毒)⇒ **上抛**,本次调用整体失败,一个字节都不发。
134
- *
135
- * `complete` 在场时同样包一层:core 的 `Brain.complete` 是 compaction/摘要腿的直调口(WebFetch 摘要器
136
- * 正是**优先**走它),漏包等于给同一个病留第二条路。
137
- */
138
22
  export function createPerModelKeyBrain(brain, getKeyResolver, opts = {}) {
139
23
  const seat = createPerModelAuthSeat(getKeyResolver, opts);
140
24
  const keyFor = async (model, given) => {
141
25
  if (given !== undefined)
142
- return given; // 已有凭据:不抢
26
+ return given;
143
27
  return (await seat(model))?.apiKey;
144
28
  };
145
29
  const stream = async (model, context, options) => {
146
30
  const apiKey = await keyFor(model, options?.apiKey);
147
31
  return brain.stream(model, context, apiKey === undefined ? options : { ...options, apiKey });
148
32
  };
149
- // 🔴 A-057.2:`.bind(brain)` **保留接收者** —— 裸摘引用(`const complete = brain.complete`)在 ESM
150
- // 严格模式下让 `this` = undefined,任何读 `this` 的 Brain 实现经本 wrapper 后当场 TypeError,
151
- // 而两行之上的 `stream` 臂是带接收者的 `brain.stream(…)`,同一只 wrapper 两条臂不该有两种调用形。
152
33
  const complete = brain.complete?.bind(brain);
153
34
  return {
154
35
  stream,
@@ -162,22 +43,6 @@ export function createPerModelKeyBrain(brain, getKeyResolver, opts = {}) {
162
43
  : {}),
163
44
  };
164
45
  }
165
- /**
166
- * 装配 side-query 执行席 = 真 `runSideQuery` + core 的 per-model auth 座,models/roles 与 Runner 同源。
167
- *
168
- * **按调用**取三件活值(`models` / `getKeyResolver` / `gatewayBaseUrl`),因为它们是**同一次** center
169
- * 下发里成对换代的(#303 codex R1-F2:目录与密钥表零 await 窗成对换代):
170
- * · `models` = `expandTiers(config.models, config.tiers)` —— 档位词 / CC 别名(`flash`/`opus`/…)在
171
- * side-query 上照常可选路(core 的 `Runner` 构造时做同一件事),且 `Runner` 那次展开是**构造时快照**,
172
- * 而本路由的模型白名单门(`routes/side-query.ts` 的 `isModelAllowlisted`)按请求现算同一张增广目录 ——
173
- * 两边取同一份才不会出现「名单放行、选路说不认识」的分歧;
174
- * · `getKeyResolver()` —— 热应用整个换 resolver 引用,活取才跟得上(装配时快照 = 旧 key 用到重启);
175
- * · `config.gatewayBaseUrl` —— off-route 判据的参照系。装配时快照会让「换网关那一刻起,这次调用把网关
176
- * 凭据发给了别家」在遥测里永久失明(钉:`test/side-query-key-plane.test.ts` 的活取钉)。
177
- *
178
- * `spec` 自带的 `getApiKeyAndHeaders`(嵌入方/装饰器)**不被覆盖** —— 它作为 `upstream` 折进本席,
179
- * 每次调用先问它,它没意见时才由本席按模型解(见 `createPerModelAuthSeat` 顶注第一条臂)。
180
- */
181
46
  export function createSideQueryLane(ctx) {
182
47
  return (spec) => runSideQuery({
183
48
  ...spec,