@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,27 +1,11 @@
1
1
  import { mysqlDriver, pgDriver, dialectJsonEncoder } from "./sql-driver.js";
2
2
  import { canAskTransition } from "../approval-ask-machine.js";
3
3
  import { isDupKeyError } from "./sql-errors.js";
4
- /**
5
- * 幂等键的入店卫生门(属主兜底轮 F2,三 twin 同判——SQL 双方言与 InMemory 都从这里过)。
6
- *
7
- * 🔴 为什么必须在店门口拒而不是「存什么比什么」:TiDB/MySQL 的 VARCHAR 等值与 UNIQUE 检查带
8
- * PAD SPACE 语义(utf8mb4_bin 也一样)——`'retry'` 与 `'retry '` 在那侧是**同一个键**,而 PG 与
9
- * InMemory 的严格字符串比较把它们当两个键 ⇒ 同一份调用方代码在不同后端拿到不同的
10
- * `idempotency_conflict`/回放结果。键是调用方铸的 opaque 值,首尾空白没有任何合法语义,
11
- * 在边界上 fail-loud 拒掉,方言分歧面整个消失(比把列改 VARBINARY 更窄、且对三形同时成立)。
12
- */
13
4
  export function assertIdempotencyKeyShape(key) {
14
5
  if (key.length === 0 || key.length > 255 || key.trim() !== key) {
15
6
  throw new Error(`invalid idempotency key: must be 1-255 chars with no leading/trailing whitespace (got ${JSON.stringify(key.slice(0, 32))}${key.length > 32 ? "…" : ""}, length ${key.length})`);
16
7
  }
17
8
  }
18
- /**
19
- * #151 车3 刀 3b(§14.2 遗留记账):两条**重放读口**被调用方的 deadline 掐断时抛的具名错误。
20
- *
21
- * 具名类而不是「按 message 文本判别」:错误文案是给人看的,拿它当控制流 = 上游改一句人话下游静默失效
22
- * (#90 工程规范 v3 门②)。调用方(开流 preamble)对它的处置与其余读错误一样 —— fail-open、零卡、一次
23
- * warn —— 所以它今天没有专门的分支;类在这里是为了让**将来**想分「超时 vs 店真报错」的消费点有得分。
24
- */
25
9
  export class ApprovalAskReadAbortedError extends Error {
26
10
  constructor(where) {
27
11
  super(`approval-ask read ${where} aborted by caller`);
@@ -31,14 +15,6 @@ export class ApprovalAskReadAbortedError extends Error {
31
15
  function abortedReadError(where) {
32
16
  return new ApprovalAskReadAbortedError(where);
33
17
  }
34
- /**
35
- * 把一次在飞的读与调用方的 `AbortSignal` 竞速(#151 车3 刀 3b)。
36
- *
37
- * 🔴 **它做到什么、做不到什么**(诚实边界,同 {@link ApprovalAskStore.listPendingBySession} 的注):
38
- * 本仓的 `SqlDriver` 没有取消 seam,所以这里能做的是「让**调用方**不再被吊住」,不是「让**库**停下来」。
39
- * 输掉竞速的那条 promise 仍在飞——它的 rejection 必须被接住(否则变成 unhandled rejection),它的成功
40
- * 结果被丢弃(纯读,零副作用)。监听器用完即摘(`once` + finally 清理),不给长命 signal 攒 listener。
41
- */
42
18
  async function withReadSignal(op, signal, where) {
43
19
  if (!signal)
44
20
  return op;
@@ -47,8 +23,8 @@ async function withReadSignal(op, signal, where) {
47
23
  onAbort = () => reject(abortedReadError(where));
48
24
  signal.addEventListener("abort", onAbort, { once: true });
49
25
  });
50
- aborted.catch(() => undefined); // 竞速赢家是 op 时,这条 promise 无人 await —— 先给它一个处理者
51
- void op.catch(() => undefined); // 同理:竞速输了的 op 的 rejection 也必须有处理者
26
+ aborted.catch(() => undefined);
27
+ void op.catch(() => undefined);
52
28
  try {
53
29
  return await Promise.race([op, aborted]);
54
30
  }
@@ -57,56 +33,8 @@ async function withReadSignal(op, signal, where) {
57
33
  signal.removeEventListener("abort", onAbort);
58
34
  }
59
35
  }
60
- // ─────────────────────────────────────────────────────────────────────────────────────────────────
61
- // DDL
62
- // ─────────────────────────────────────────────────────────────────────────────────────────────────
63
36
  export const APPROVAL_ASK_TABLE = "approval_ask";
64
37
  export const APPROVAL_BATCH_TABLE = "approval_batch";
65
- /**
66
- * TiDB/MySQL-protocol 侧的两条建表语句,**抽成导出数组**(车3 刀 3a):`tidb-pool.ts` 的
67
- * `SCHEMA_STATEMENTS` 用 `...TIDB_APPROVAL_ASK_STATEMENTS` 展开它们,于是这两张表随中央
68
- * `ensureSchema` 一起在 **named-lock 的那条 `conn`** 上建 —— 把一个 pool 级 ensure 函数塞进
69
- * `SCHEMA_STATEMENTS` 的执行循环里做不到这点(会绕开 DDL 串行化),所以真源必须是**语句文本**。
70
- * 下面的 `ensureTiDBApprovalAskSchema` 保留为遍历这个数组的薄壳(集成测试直接调它建表,
71
- * 不必为了建两张表去拉起整套中央 schema)。
72
- *
73
- * SCHEMA POLICY 同 `tidb-pool.ts` 头注:纯 CREATE,禁 ALTER 增量 seam,改列直接改这里 + 重建库
74
- * (`docs/schema/baseline-mysql.sql` 是由这些文本机器录制出的**产物**,永不手改)。
75
- *
76
- * ── 列宽的**测量依据**(#192 A10;U2 逐列取证在档)──────────────────────────────────────────────────
77
- * 本表家族原先一律 VARCHAR(255),是"先建表后想"的产物。按"每列的值到底由谁铸、有没有入口上限"重新裁:
78
- *
79
- * 收窄(有硬上限背书,证据即上限本身):
80
- * · `ask_id` / `batch_id` / `bound_ask_id` → 64:`deriveAskId`/`deriveBatchId` 恒是 sha256 hex 取前
81
- * 64 字符(`approval-ask-machine.ts` deterministicId),按构造不可能更长。
82
- * · `task_id` → 64:入口 `UUIDV7_RE`(`security.ts:134`)只放行 36 字符的规范 uuidv7;与兄弟列
83
- * `task_run.task_id VARCHAR(64)` 同宽(此前 255 是本家族独有的偏离,无依据)。
84
- * · `session_id` → 64:提交入口硬拒 >64(`http/server.ts` "sessionId must be at most 64 characters"),
85
- * 与 `session_meta`/`task_run` 同宽。
86
- * · `owner` → 190:`assertPrincipalShape` 的 `PRINCIPAL_MAX_LENGTH = 190`(`security.ts` 的同名导出常量,
87
- * 刻意不钉行号 —— 旧注写的 `security.ts:350` 已随该文件编辑漂走)硬拒更长者;
88
- * 全仓每一根 owner/scope 轴都是 190,此前 255 同样是无依据偏离。
89
- * · `gate_token` → 120:它抄的是 checkpoint 的 token(core `mintCheckpointToken` = 16 字节 hex = 32 字符),
90
- * 取与**被抄那一列** `checkpoint.token VARCHAR(120)` 同宽 —— 同一个值在两张表上宽度必须一致,
91
- * 否则抄的那一步就是一道静默截断。512 是原先的 16 倍冗余。
92
- *
93
- * 🔑 上面四条收窄之所以**不引入任何新暴露面**,靠的不是入口断言而是一条更硬的事实:同一个 session_id /
94
- * task_id / owner **早就**同时躺在 `session_meta.session_id VARCHAR(64)` / `task_run.task_id VARCHAR(64)` /
95
- * `task_run.owner VARCHAR(190)` 里。任何长到能撑爆本表新宽度的值,在写到那几张**更中心**的表时就已经
96
- * 先炸了 —— 本表此前的 255 从来不是一道额外的安全余量,只是一处与全仓不一致的偏离。
97
- * ⇒ 收窄的风险上界 = 0;真正要担心的是反过来:留着 255 会让人误以为这里可以存更长的 id。
98
- *
99
- * 🔴 **刻意不收窄**(测量结果不支持,记在这里免得下一个人以为是漏了):
100
- * · `source_task_id` / `tool_call_id` / `parent_tool_call_id` / `gate_bound_call_id` —— 这四列的值是
101
- * **模型/引擎铸的原始 id**,服务端在写行这一步**没有任何长度断言**(`approval-card.ts` 的
102
- * `MAX_IDENT` 只裁了给人看的那份 card 投影,不是本列)。没有入口上限就收窄 = 把"存不下"这件事
103
- * 推迟到 INSERT 才炸(MySQL 更糟:静默截断),换来的只是几十字节。要收窄,先补入口断言
104
- * (照 `assertPrincipalShape`/`assertIdempotencyKeyShape` 的姿势),那是另一件事。
105
- * · `bound_input_hash` / `gate_bound_input_hash` —— 按约定是 sha256 hex(64),但 `readBoundInputHash`
106
- * 只校验"非空字符串",没校长度/字形 ⇒ 同上,先有断言再谈收窄。
107
- * · `idempotency_key` —— 255 **正是**入口断言的上限本身(zod `.max(255)` + 店内 `assertIdempotencyKeyShape`
108
- * 双执法),宽度与上限同源,恰好正确,动它反而制造截断面。
109
- */
110
38
  export const TIDB_APPROVAL_ASK_STATEMENTS = [
111
39
  `CREATE TABLE IF NOT EXISTS ${APPROVAL_BATCH_TABLE} (
112
40
  batch_id VARCHAR(64) NOT NULL,
@@ -168,8 +96,6 @@ export const TIDB_APPROVAL_ASK_STATEMENTS = [
168
96
  KEY idx_ask_state (state)
169
97
  ) COLLATE utf8mb4_bin`,
170
98
  ];
171
- /** {@link TIDB_APPROVAL_ASK_STATEMENTS} 的遍历壳——生产路径走 `tidb-pool.ts` 的中央 `ensureSchema`
172
- * (那里展开了同一份数组),本函数留给只需要这两张表的集成测试。 */
173
99
  export async function ensureTiDBApprovalAskSchema(pool) {
174
100
  for (const stmt of TIDB_APPROVAL_ASK_STATEMENTS)
175
101
  await pool.query(stmt);
@@ -223,8 +149,6 @@ export async function ensurePgApprovalAskSchema(q) {
223
149
  CONSTRAINT uq_approval_ask_idem UNIQUE (task_id, idempotency_key)
224
150
  )`);
225
151
  await q(`CREATE INDEX IF NOT EXISTS idx_ask_task_state ON ${APPROVAL_ASK_TABLE} (task_id, state)`);
226
- // 车3 §5.1:sync 腿开流重放读口(listPendingBySession)的支撑索引 —— MySQL twin 的
227
- // `KEY idx_ask_session_state (session_id, state)` 同形。
228
152
  await q(`CREATE INDEX IF NOT EXISTS idx_ask_session_state ON ${APPROVAL_ASK_TABLE} (session_id, state)`);
229
153
  await q(`CREATE INDEX IF NOT EXISTS idx_ask_batch ON ${APPROVAL_ASK_TABLE} (batch_id)`);
230
154
  await q(`CREATE INDEX IF NOT EXISTS idx_ask_state ON ${APPROVAL_ASK_TABLE} (state)`);
@@ -234,10 +158,6 @@ const ASK_COLS = "ask_id, task_id, source_task_id, session_id, owner, batch_id,
234
158
  "gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, " +
235
159
  "created_at_ms, updated_at_ms";
236
160
  const BATCH_COLS = "batch_id, task_id, state, bound_ask_id, rev, created_at_ms, updated_at_ms";
237
- /** JSON 列的读出:**本层只做存取,不解释**——返回类型即 `unknown`,把「这是什么形」的责任留给消费方。
238
- * 两个 JSON 列的 schema 属主都不在本车:`decision_actor` = design/171 ActorAssertion 形(车4 回决端点
239
- * 消费,届时按宪法 [2704] 立 schema 后 safeParse);`card_json` = 172 §3.1 呈卡帧载荷(车3 wire 面属主)。
240
- * 故此处刻意不铸内域类型(无 as),持久层对这两列保持透明字节存取语义。 */
241
161
  function parseJsonColumn(v) {
242
162
  return JSON.parse(String(v));
243
163
  }
@@ -292,8 +212,6 @@ function mapBatchRow(r) {
292
212
  updatedAtMs: Number(r.updated_at_ms),
293
213
  };
294
214
  }
295
- /** 动态 SET 列清单(`transitionAsk`/`resolveProvisional` 用):只收 patch 里真出现的字段,`decisionActor`
296
- * 按内容面 JSON 编码器序列化。`updatedAtMs` 不在这里——调用方把它拼进固定尾列(所有转移恒推进它)。 */
297
215
  function patchColumns(patch, json) {
298
216
  const out = [];
299
217
  if (patch.decision !== undefined)
@@ -316,14 +234,9 @@ function patchColumns(patch, json) {
316
234
  out.push(["provisional", patch.provisional ? 1 : 0]);
317
235
  return out;
318
236
  }
319
- /** 第 n 个(1-based)绑定参数的占位符文本。TiDB 用位置序 `?`(n 被忽略);PG 需要显式编号 `$n`——
320
- * 两种转移(transitionAsk/resolveProvisional)的列集合按 patch 内容动态变化,固定的两段式 SQL 文本
321
- * 写不出来,因此占位符本身也必须按方言现场生成(与 background-agent-store-sql.ts 的 guard 占位符
322
- * 构造同精神:方言差异仍然显式——分支在这一个函数里,不是被某个跨方言的抽象吞掉)。 */
323
237
  function ph(dialect, n) {
324
238
  return dialect === "tidb" ? "?" : `$${n}`;
325
239
  }
326
- /** 双方言 `ApprovalAskStore`。见文件头注的 CAS 铁则 + 方言差异清单。 */
327
240
  export class SqlApprovalAskStore {
328
241
  db;
329
242
  constructor(db) {
@@ -335,44 +248,24 @@ export class SqlApprovalAskStore {
335
248
  json(value) {
336
249
  return dialectJsonEncoder(this.db.dialect)(value);
337
250
  }
338
- /** 唯一键冲突判别 —— 判据属主 = `sql-errors.ts`(A-032 P1-①)。`decideAsk` 用它把 `(task_id,
339
- * idempotency_key)` 撞键翻成 typed 的 `idempotency_conflict` 臂,不让驱动错误对象漏给消费层;
340
- * 旧形 tidb 臂只认 errno,只带 `code` 的驱动错误会漏判成"真障碍"。 */
341
251
  isDupKey(e) {
342
252
  return isDupKeyError(this.db.dialect, e);
343
253
  }
344
- /**
345
- * 🔴 失败臂的回读**必须走当前这条 `conn`**,不许调 `this.getAsk`/`this.getBatch`(codex 复审 F1,
346
- * 2026-08-06 真缺陷,池大小=1 的钉上实测挂死 25s)。原因:那两个读口走**池级** `this.db.query`,而
347
- * 此刻本方法还攥着自己的事务连接(要到 `finally` 才 release)——于是「回读在等一条新连接,而唯一
348
- * 那条连接要等回读返回才释放」= 确定性死锁。池大于 1 时它退化成「同时走到失败臂的并发数 ≥ 池大小
349
- * 就整池饿死」,是同一个缺陷的更难复现形,不是另一个问题。
350
- *
351
- * 调用点纪律:**先 `conn.rollback()` 再调这两个**。回滚之后连接回到 autocommit,这条 SELECT 自成
352
- * 一个事务 ⇒ 读到的是最新已提交视图(正是失败臂要如实上报的东西),不受本事务快照的影响——两方言
353
- * 在这一点上同义,不必再纠结 InnoDB 读视图与 TiDB start_ts 的差别。
354
- */
355
254
  async getAskOn(conn, askId) {
356
255
  const { rows } = await conn.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE ask_id = $1`), [askId]);
357
256
  return rows[0] ? mapAskRow(rows[0]) : null;
358
257
  }
359
- /** {@link getAskOn} 的批侧同形(同一条纪律:失败臂回读走本连接)。 */
360
258
  async getBatchOn(conn, batchId) {
361
259
  const { rows } = await conn.query(this.q(`SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCH_TABLE} WHERE batch_id = ?`, `SELECT ${BATCH_COLS} FROM ${APPROVAL_BATCH_TABLE} WHERE batch_id = $1`), [batchId]);
362
260
  return rows[0] ? mapBatchRow(rows[0]) : null;
363
261
  }
364
262
  async ensureAsk(row) {
365
263
  const now = row.createdAtMs;
366
- /** 本次 ask 插入的 affected 行数(1 = 真插入 / 0 = 幂等命中)——判别位的**唯一**来源,见
367
- * {@link EnsureAskResult}。两方言各自的语句形不同(`INSERT IGNORE` vs `ON CONFLICT DO NOTHING`),
368
- * 但 affected 的语义是同一个,`SqlDriver` 已把 mysql2 的 `affectedRows` 与 pg 的 `rowCount` 归一。 */
369
264
  let insertedRows = 0;
370
265
  const conn = await this.db.connect();
371
266
  try {
372
267
  await conn.begin();
373
268
  await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES (?, ?, 'OPEN', 0, ?, ?)`, `INSERT INTO ${APPROVAL_BATCH_TABLE} (batch_id, task_id, state, rev, created_at_ms, updated_at_ms) VALUES ($1, $2, 'OPEN', 0, $3, $4) ON CONFLICT (batch_id) DO NOTHING`), [row.batchId, row.taskId, now, now]);
374
- // 🔴 `idempotency_key` **不在 INSERT 列清单里**(车4 §12-E 双写者收口):该列已归回决专用,只有
375
- // `decideAsk` 赢 CAS 时写它,铸行恒 NULL。留在这里的 NULL 字面量就是这条纪律的落笔处。
376
269
  const askInsert = await conn.query(this.q(`INSERT IGNORE INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
377
270
  "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?)", `INSERT INTO ${APPROVAL_ASK_TABLE} (ask_id, task_id, source_task_id, session_id, owner, batch_id, tool_call_id, leg_key, parent_tool_call_id, bound_input_hash, state, provisional, rev, decision, decision_actor, decision_note, decided_at_ms, denied_reason, gate_token, gate_bound_call_id, gate_bound_input_hash, idempotency_key, card_json, schema_version, expires_at_ms, created_at_ms, updated_at_ms) ` +
378
271
  "VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, 'STREAM_PENDING', 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, $11, $12, $13, $14, $15) ON CONFLICT (ask_id) DO NOTHING"), [
@@ -400,7 +293,6 @@ export class SqlApprovalAskStore {
400
293
  await conn.rollback();
401
294
  }
402
295
  catch {
403
- /* ignore */
404
296
  }
405
297
  throw err;
406
298
  }
@@ -410,12 +302,9 @@ export class SqlApprovalAskStore {
410
302
  const out = await this.getAsk(row.askId);
411
303
  if (!out)
412
304
  throw new Error(`ensureAsk: row vanished after insert (askId=${row.askId})`);
413
- // 判别位取**提交那一刻**引擎报的 affected,不是事后回读推断的:回读发生在提交之后,那时并发的
414
- // 第二个调用可能已经改过行,任何「从行上反推谁插的」都是又一次猜。
415
305
  return { row: out, inserted: insertedRows > 0 };
416
306
  }
417
307
  async transitionAsk(askId, from, to, patch) {
418
- // machine 层先判(非法转移=编程错误,throw 而非返回 false——设计定稿 §4 原话)。
419
308
  if (!canAskTransition(from, to)) {
420
309
  throw new Error(`approval-ask-store.transitionAsk: illegal ask transition ${from} -> ${to} (askId=${askId})`);
421
310
  }
@@ -427,33 +316,26 @@ export class SqlApprovalAskStore {
427
316
  const setSql = cols.map((c) => `${c} = ${ph(dialect, ++n)}`).join(", ");
428
317
  const askIdPh = ph(dialect, ++n);
429
318
  const fromPh = ph(dialect, ++n);
430
- // `rev = rev + 1`(恒变列,MySQL 同值 UPDATE affectedRows=0 坑——见文件头注 CAS 铁则②)。
431
319
  const res = await this.db.query(`UPDATE ${APPROVAL_ASK_TABLE} SET ${setSql}, rev = rev + 1 WHERE ask_id = ${askIdPh} AND state = ${fromPh}`, [...vals, askId, from]);
432
320
  return res.affected === 1;
433
321
  }
434
322
  async decideAsk(askId, batchId, decision) {
435
323
  if (decision.idempotencyKey != null)
436
- assertIdempotencyKeyShape(decision.idempotencyKey); // F2 卫生门:进事务前拒
324
+ assertIdempotencyKeyShape(decision.idempotencyKey);
437
325
  const now = Date.now();
438
326
  const conn = await this.db.connect();
439
327
  try {
440
328
  await conn.begin();
441
- // ① 批必须还 OPEN(自增写 rev 保证 affected 语义在 CLIENT_FOUND_ROWS 关闭下仍=1;=0 ⇒ 批已封)。
442
329
  const batchRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET rev = rev + 1, updated_at_ms = ? WHERE batch_id = ? AND state = 'OPEN'`, `UPDATE ${APPROVAL_BATCH_TABLE} SET rev = rev + 1, updated_at_ms = $1 WHERE batch_id = $2 AND state = 'OPEN'`), [now, batchId]);
443
330
  if (batchRes.affected !== 1) {
444
331
  await conn.rollback();
445
332
  return { ok: false, reason: "batch_closed", row: await this.getAskOn(conn, askId) };
446
333
  }
447
- // ② ask 必须还 STREAM_PENDING;单赢者 CAS。回决幂等键随决议**同一条语句**落列(车4 §12-E):
448
- // 分两步写会开一个「已 DECIDED 但键还没落」的窗口,同 key 重试在窗口内读不到键 ⇒ 会去打第二次
449
- // CAS(输)再报 409,把一次合法回放变成冲突。同一 UPDATE ⇒ 键与终态同生共死。
450
- let askRes; // 显式标注:`let x;` 是 evolving-any,本仓按强类型用
334
+ let askRes;
451
335
  try {
452
336
  askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=?, decision_actor=?, decision_note=?, decided_at_ms=?, idempotency_key=?, rev=rev+1, updated_at_ms=? WHERE ask_id=? AND state='STREAM_PENDING'`, `UPDATE ${APPROVAL_ASK_TABLE} SET state='DECIDED', decision=$1, decision_actor=$2, decision_note=$3, decided_at_ms=$4, idempotency_key=$5, rev=rev+1, updated_at_ms=$6 WHERE ask_id=$7 AND state='STREAM_PENDING'`), [decision.decision, decision.decisionActor == null ? null : this.json(decision.decisionActor), decision.decisionNote ?? null, now, decision.idempotencyKey ?? null, now, askId]);
453
337
  }
454
338
  catch (err) {
455
- // `(task_id, idempotency_key)` 撞键 = 同一 key 已被本 task 内**另一条** ask 的回决占用。这是调用方
456
- // 键管理的问题(不是 CAS 竞争),翻成 typed 臂交给端点译 409——方言 errno/SQLSTATE 不出店。
457
339
  if (this.isDupKey(err)) {
458
340
  await conn.rollback();
459
341
  return { ok: false, reason: "idempotency_conflict", row: await this.getAskOn(conn, askId) };
@@ -471,7 +353,6 @@ export class SqlApprovalAskStore {
471
353
  await conn.rollback();
472
354
  }
473
355
  catch {
474
- /* ignore */
475
356
  }
476
357
  throw err;
477
358
  }
@@ -488,39 +369,23 @@ export class SqlApprovalAskStore {
488
369
  const conn = await this.db.connect();
489
370
  try {
490
371
  await conn.begin();
491
- // ⓪ 锁序不变量(真双库 §6-4b 死锁实抓,2026-08-05):凡同一事务触碰批行+ask 行者,**一律批行先**
492
- // (decideAsk/bindBatch/abortBatch 皆批先;expireAsk 原是 ask 先 ⇒ 与 decideAsk 构成 AB-BA,
493
- // TiDB 悲观锁当场 `Deadlock found`)。这里用无谓词的 rev 自增先取批行锁作序列化点——
494
- // 带 state 谓词的转移放后面(谓词不符时部分引擎不留锁,序列化点会失效)。ask CAS 输时整个
495
- // 事务回滚,这次自增一并撤销,无痕。
496
372
  const lockRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET rev=rev+1 WHERE batch_id=?`, `UPDATE ${APPROVAL_BATCH_TABLE} SET rev=rev+1 WHERE batch_id=$1`), [batchId]);
497
373
  if (lockRes.affected !== 1) {
498
- await conn.rollback(); // 批行不存在 = ensureAsk 从未为它建批(编程错误形,如实不赢)
374
+ await conn.rollback();
499
375
  return { won: false, voidedSiblings: [] };
500
376
  }
501
- // ① 中选:ask STREAM_PENDING → PARKING(单赢者;输 = 已被回决/取消,整体放弃)。
502
377
  const askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKING', rev=rev+1, updated_at_ms=? WHERE ask_id=? AND state='STREAM_PENDING'`, `UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKING', rev=rev+1, updated_at_ms=$1 WHERE ask_id=$2 AND state='STREAM_PENDING'`), [now, askId]);
503
378
  if (askRes.affected !== 1) {
504
379
  await conn.rollback();
505
380
  return { won: false, voidedSiblings: [] };
506
381
  }
507
- // ② 批 OPEN → ROUTING_UNBOUND(已是 ROUTING_UNBOUND = 兄弟先到,affected=0 也算成功续走③——不检查
508
- // affected,批状态无论如何这一刻都已经是 ROUTING_UNBOUND 或更前进)。
509
382
  await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=? WHERE batch_id=? AND state='OPEN'`, `UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_UNBOUND', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='OPEN'`), [now, batchId]);
510
- // ③ 兄弟撤卡:PG 用 RETURNING 单语句拿名单;TiDB 无 RETURNING,同事务先 SELECT 后 UPDATE。
511
383
  let voidedSiblings;
512
384
  if (this.db.dialect === "pg") {
513
385
  const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId]);
514
386
  voidedSiblings = res.rows.map((r) => String(r.ask_id));
515
387
  }
516
388
  else {
517
- // 🔴 `FOR UPDATE` + 按选出的 id 列表更新(车5 同病修,与 `bindBatch` 的 PARKING 臂逐字同形——
518
- // 那里的完整论证见该处注):裸 SELECT 是**非锁定读**,它与随后那条宽谓词 UPDATE 不是同一个行集
519
- // (`transitionAsk`/`decideAsk` 不走批锁,可以在两条语句之间把某个兄弟搬进/搬出 STREAM_PENDING)。
520
- // 搬进的会被 UPDATE 改成 VOID 却**不在**返回名单里(撤卡帧漏发 ⇒ 壳上留一张已作废的卡),搬出的
521
- // 会在名单里却没被改(撤卡帧误发)。PG 的 RETURNING 与 InMemory 都返回「真正被改的那一组」,
522
- // 所以裸读会造出一个**只在 MySQL 方言上**成立的协议分歧。锁序:本方法已先取批行锁(步⓪),
523
- // 此处再取 ask 行锁,与文件头注的「批先 ask 后」一致。
524
389
  const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASK_TABLE} WHERE batch_id=? AND state='STREAM_PENDING' AND ask_id<>? FOR UPDATE`, [batchId, askId]);
525
390
  voidedSiblings = sel.rows.map((r) => String(r.ask_id));
526
391
  if (voidedSiblings.length > 0) {
@@ -539,7 +404,6 @@ export class SqlApprovalAskStore {
539
404
  await conn.rollback();
540
405
  }
541
406
  catch {
542
- /* ignore */
543
407
  }
544
408
  throw err;
545
409
  }
@@ -552,44 +416,25 @@ export class SqlApprovalAskStore {
552
416
  const conn = await this.db.connect();
553
417
  try {
554
418
  await conn.begin();
555
- // bind-once:批 ROUTING_UNBOUND → ROUTING_BOUND + bound_ask_id(WHERE bound_ask_id IS NULL 是第二道
556
- // 保险——state 谓词已经把并发者挡在外面,这里双保险防「同一批被 bind 两次」哪怕未来状态机被误改)。
557
419
  const batchRes = await conn.query(this.q(`UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_BOUND', bound_ask_id=?, rev=rev+1, updated_at_ms=? WHERE batch_id=? AND state='ROUTING_UNBOUND' AND bound_ask_id IS NULL`, `UPDATE ${APPROVAL_BATCH_TABLE} SET state='ROUTING_BOUND', bound_ask_id=$1, rev=rev+1, updated_at_ms=$2 WHERE batch_id=$3 AND state='ROUTING_UNBOUND' AND bound_ask_id IS NULL`), [askId, now, batchId]);
558
420
  if (batchRes.affected !== 1) {
559
- // 判别式的信息来源 = 批行的真实态。🔴 回读**放在 rollback 之后、走池上的独立读**,不在本事务内读:
560
- // TiDB 的事务快照按 start_ts 取,同事务内的普通 SELECT 可能读到 BEGIN 时刻的旧值(InnoDB 的读视图
561
- // 规则又与它不同)——两方言在这一点上不等价,而本车恰恰要靠这个值分 ABORTED / ROUTING_BOUND 两条
562
- // **处置相反**的臂。事务已回滚(本事务除了那条 affected=0 的 CAS 什么也没写),池上重读是干净的
563
- // 已提交视图,双方言同义。
564
421
  await conn.rollback();
565
422
  const cur = await this.getBatchOn(conn, batchId);
566
423
  if (!cur)
567
424
  return { ok: false, batchState: "MISSING" };
568
425
  return { ok: false, batchState: cur.state, ...(cur.boundAskId !== null ? { boundAskId: cur.boundAskId } : {}) };
569
426
  }
570
- // 中选 ask PARKING → PARKED(带 gate 三件)。
571
427
  const askRes = await conn.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKED', gate_token=?, gate_bound_call_id=?, gate_bound_input_hash=?, rev=rev+1, updated_at_ms=? WHERE ask_id=? AND state='PARKING'`, `UPDATE ${APPROVAL_ASK_TABLE} SET state='PARKED', gate_token=$1, gate_bound_call_id=$2, gate_bound_input_hash=$3, rev=rev+1, updated_at_ms=$4 WHERE ask_id=$5 AND state='PARKING'`), [gate.gateToken, gate.gateBoundCallId ?? null, gate.gateBoundInputHash ?? null, now, askId]);
572
428
  if (askRes.affected !== 1) {
573
429
  await conn.rollback();
574
- // ask 不在 PARKING(被别的竞争者收走)⇒ 整事务回滚,批**没有**被绑上。如实报批的回滚后真值:
575
- // 它仍是 ROUTING_UNBOUND(上一步的 CAS 谓词已经证明了这一点,连同本次自增一起撤销)。
576
430
  return { ok: false, batchState: "ROUTING_UNBOUND" };
577
431
  }
578
- // 其余 PARKING 兄弟 → VOID,**名单随成功臂回传**(车5 §8 A-4):撤卡帧的 `askIds` 就是它。
579
- // 读法与 expireAsk 逐字同形:PG `RETURNING` 单语句;TiDB 无 RETURNING ⇒ 同事务先 SELECT 后 UPDATE。
580
432
  let voidedSiblings;
581
433
  if (this.db.dialect === "pg") {
582
434
  const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='PARKING' AND ask_id<>$3 RETURNING ask_id`, [now, batchId, askId]);
583
435
  voidedSiblings = res.rows.map((r) => String(r.ask_id));
584
436
  }
585
437
  else {
586
- // 🔴 `FOR UPDATE` + 按选出的 id 逐条更新(codex 复审 F2,2026-08-06):裸 SELECT 是非锁定读,
587
- // 它与随后那条宽谓词 UPDATE **不是**同一个行集——`transitionAsk` 不走批锁,可以在两条语句之间
588
- // 把某个兄弟搬进/搬出 PARKING。搬进的会被 UPDATE 改成 VOID 却**不在**返回名单里(撤卡帧漏发 ⇒
589
- // 壳上留一张已作废的卡),搬出的会在名单里却没被改(撤卡帧误发)。PG 的 RETURNING 与 InMemory
590
- // 都返回「真正被改的那一组」,所以裸读会造出一个**只在 MySQL 方言上**成立的协议分歧。
591
- // 修法:锁定读把行集钉死,再只更新这批 id ⇒ 返回名单恒等于被改集合,三形同义。
592
- // 锁序:本方法已先取批行锁(上面那条 CAS),此处再取 ask 行锁,与文件头注的「批先 ask 后」一致。
593
438
  const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASK_TABLE} WHERE batch_id=? AND state='PARKING' AND ask_id<>? FOR UPDATE`, [batchId, askId]);
594
439
  voidedSiblings = sel.rows.map((r) => String(r.ask_id));
595
440
  if (voidedSiblings.length > 0) {
@@ -605,7 +450,6 @@ export class SqlApprovalAskStore {
605
450
  await conn.rollback();
606
451
  }
607
452
  catch {
608
- /* ignore */
609
453
  }
610
454
  throw err;
611
455
  }
@@ -623,17 +467,12 @@ export class SqlApprovalAskStore {
623
467
  await conn.rollback();
624
468
  return [];
625
469
  }
626
- // 只撤 STREAM_PENDING(设计定稿字面:「整批 STREAM_PENDING→VOID」)——已 DECIDED 的兄弟不受影响
627
- // (§3.0 原话),PARKING/PARKED/DENIED 同样不在这条 UPDATE 的谓词里,保持不动。
628
470
  let voided;
629
471
  if (this.db.dialect === "pg") {
630
472
  const res = await conn.query(`UPDATE ${APPROVAL_ASK_TABLE} SET state='VOID', rev=rev+1, updated_at_ms=$1 WHERE batch_id=$2 AND state='STREAM_PENDING' RETURNING ask_id`, [now, batchId]);
631
473
  voided = res.rows.map((r) => String(r.ask_id));
632
474
  }
633
475
  else {
634
- // 🔴 同 `expireAsk`/`bindBatch` 的锁定读改造(车5 同病修):非锁定读的名单与宽谓词 UPDATE 的
635
- // 行集不是同一个 ⇒ 撤卡帧漏发/误发,且分歧只在 MySQL 方言上成立。锁序:批行锁(上一条 CAS)
636
- // 已取,此处再取 ask 行锁。
637
476
  const sel = await conn.query(`SELECT ask_id FROM ${APPROVAL_ASK_TABLE} WHERE batch_id=? AND state='STREAM_PENDING' FOR UPDATE`, [batchId]);
638
477
  voided = sel.rows.map((r) => String(r.ask_id));
639
478
  if (voided.length > 0) {
@@ -649,7 +488,6 @@ export class SqlApprovalAskStore {
649
488
  await conn.rollback();
650
489
  }
651
490
  catch {
652
- /* ignore */
653
491
  }
654
492
  throw err;
655
493
  }
@@ -657,17 +495,6 @@ export class SqlApprovalAskStore {
657
495
  conn.release();
658
496
  }
659
497
  }
660
- /**
661
- * 🔴 排序键是 `updated_at_ms`(车5 §8 D-4;codex 三轮补抓:原先按 `created_at_ms` 排,`deferReconcile`
662
- * 就是**空转**的)。这条读口与 `deferReconcile` 是一对:收敛器每轮取前 `limit` 条,判不出终局的行用
663
- * `deferReconcile` 推 `updated_at_ms` 把自己排到队尾,下一轮新行才轮得到。若这里仍按建行时间排,
664
- * 推 `updated_at_ms` 对顺序毫无影响 ⇒ 一旦 `limit` 被若干长驻 PARKING 行占满,每轮扫到的永远是同一批,
665
- * 后来的审批被无限饿死(§5 旋钮那句「超出下轮接着扫」也就不成立)。
666
- * 次级键 `created_at_ms` 只为**确定性**:同毫秒的行不至于在两次扫描间乱序(分页/限流下的稳定序)。
667
- *
668
- * 🔴 配套纪律(§9 C5):任何 TTL 判据一律量 immutable 的 `createdAtMs`/`expiresAtMs`,**绝不量
669
- * `updatedAtMs`**——它被队列轮转每轮刷新,量它的 TTL 永不到期。
670
- */
671
498
  async listByState(state, limit) {
672
499
  const { rows } = await this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE state = ? ORDER BY updated_at_ms ASC, created_at_ms ASC LIMIT ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE state = $1 ORDER BY updated_at_ms ASC, created_at_ms ASC LIMIT $2`), [state, limit]);
673
500
  return rows.map(mapAskRow);
@@ -681,8 +508,6 @@ export class SqlApprovalAskStore {
681
508
  async listPendingBySession(sessionId, owner, signal) {
682
509
  if (signal?.aborted === true)
683
510
  throw abortedReadError("listPendingBySession");
684
- // 租户门的两段谓词文本(见接口注):`owner = ?` 对 NULL 恒 UNKNOWN ⇒ 无租户身份的部署形必须走
685
- // `owner IS NULL`,否则这条读口在那些部署上恒回零行(一条静默失效的重放面,比报错更难发现)。
686
511
  const { rows } = await withReadSignal(this.db.query(owner === null
687
512
  ? this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner IS NULL ORDER BY created_at_ms ASC`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner IS NULL ORDER BY created_at_ms ASC`)
688
513
  : this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = ? AND state = 'STREAM_PENDING' AND owner = ? ORDER BY created_at_ms ASC`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE session_id = $1 AND state = 'STREAM_PENDING' AND owner = $2 ORDER BY created_at_ms ASC`), owner === null ? [sessionId] : [sessionId, owner]), signal, "listPendingBySession");
@@ -695,20 +520,15 @@ export class SqlApprovalAskStore {
695
520
  return rows[0] ? mapAskRow(rows[0]) : null;
696
521
  }
697
522
  async getByIdempotencyKey(taskId, key) {
698
- assertIdempotencyKeyShape(key); // F2 卫生门:坏形键不做注定方言分歧的查询
699
- // 谓词跟 UNIQUE `(task_id, idempotency_key)` 逐字对齐 ⇒ 至多一行,且天然带租户门(见接口注)。
523
+ assertIdempotencyKeyShape(key);
700
524
  const { rows } = await this.db.query(this.q(`SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = ? AND idempotency_key = ?`, `SELECT ${ASK_COLS} FROM ${APPROVAL_ASK_TABLE} WHERE task_id = $1 AND idempotency_key = $2`), [taskId, key]);
701
525
  return rows[0] ? mapAskRow(rows[0]) : null;
702
526
  }
703
527
  async deferReconcile(askId, expectedState, expectedRev, nowMs) {
704
- // 纯 touch:**不碰 state**(见接口注——同态转移在状态机表上无出边,这里刻意走另一扇门),只推
705
- // `updated_at_ms` 与恒变的 `rev`。`AND state=? AND rev=?` 双谓词 = 两副本同扫时的单赢者门。
706
528
  const res = await this.db.query(this.q(`UPDATE ${APPROVAL_ASK_TABLE} SET updated_at_ms = ?, rev = rev + 1 WHERE ask_id = ? AND state = ? AND rev = ?`, `UPDATE ${APPROVAL_ASK_TABLE} SET updated_at_ms = $1, rev = rev + 1 WHERE ask_id = $2 AND state = $3 AND rev = $4`), [nowMs, askId, expectedState, expectedRev]);
707
529
  return res.affected === 1;
708
530
  }
709
531
  async resolveProvisional(askId, from, to, patch) {
710
- // 🔴 故意不经 canAskTransition:这是设计定稿 §3.0/§4 明说的「provisional 行的版本化补偿」——终态→终态
711
- // 的例外通道,机器表把终态定义为「无出边」,这里走的是另一扇门(WHERE provisional=1 AND state=from)。
712
532
  const dialect = this.db.dialect;
713
533
  const dyn = patchColumns(patch, (v) => this.json(v));
714
534
  const cols = ["state", "provisional", ...dyn.map(([c]) => c), "updated_at_ms"];
@@ -730,13 +550,11 @@ export class SqlApprovalAskStore {
730
550
  return rows[0] ? mapBatchRow(rows[0]) : null;
731
551
  }
732
552
  }
733
- /** MySQL-protocol(TiDB)绑定。 */
734
553
  export class TiDBApprovalAskStore extends SqlApprovalAskStore {
735
554
  constructor(pool) {
736
555
  super(mysqlDriver(pool));
737
556
  }
738
557
  }
739
- /** PostgreSQL 绑定。 */
740
558
  export class PgApprovalAskStore extends SqlApprovalAskStore {
741
559
  constructor(pool) {
742
560
  super(pgDriver(pool));
@@ -1,22 +1,6 @@
1
- /**
2
- * Per-session per-toolName approval exemption ("本会话不再询问",
3
- * design: sema-internal server/docs/design/APPROVAL-SESSION-EXEMPTION.md).
4
- *
5
- * SEMANTICS (the boundary that keeps this an APPROVAL MEMORY, not a policy loosen): a granted row makes the
6
- * F4 ask-gate SKIP the human ask for that (sessionId, canonicalToolName) — it is consulted ONLY after
7
- * `APPROVAL_DENY` and `APPROVAL_NEVER_AUTO` (both always win), and never touches the session-policy layer
8
- * (tighten-only stays intact). Keyed in CANONICAL tool-name space (the same rename-smuggling discipline as
9
- * the require/deny sets). Lifecycle = session-bound: E21 session DELETE purges via deleteBySession.
10
- *
11
- * Four twins behind one interface (the resume-anchor pattern): TiDB/PG = durable cross-replica SQL rows
12
- * (idempotent upsert on the (session_id, tool_name) PK); File = per-session JSONL append log (fsync — a
13
- * lost grant re-asks, but a torn log must not FABRICATE one), hydrated on boot, torn-tail-safe; Memory =
14
- * tests / ephemeral. Consumers treat store FAILURE as fail-closed (ask anyway) — enforced at the call site.
15
- */
16
1
  import { join } from "node:path";
17
2
  import { existsSync, readdirSync, rmSync } from "node:fs";
18
3
  import { AppendLog, ensureDir, readJsonlRecords, sanitizePathComponent } from "@sema-agent/core";
19
- // ─── TiDB / MySQL twin ───────────────────────────────────────────────────────────────────────────────────
20
4
  export class TiDBApprovalExemptionStore {
21
5
  pool;
22
6
  constructor(pool) {
@@ -24,8 +8,7 @@ export class TiDBApprovalExemptionStore {
24
8
  }
25
9
  async grant(sessionId, toolName, grantedBy) {
26
10
  await this.pool.query("INSERT INTO approval_exemption (session_id, tool_name, granted_by, created_at) VALUES (?,?,?,?) " +
27
- "ON DUPLICATE KEY UPDATE granted_by = VALUES(granted_by)", // created_at keeps the first grant
28
- [sessionId, toolName, grantedBy, new Date()]);
11
+ "ON DUPLICATE KEY UPDATE granted_by = VALUES(granted_by)", [sessionId, toolName, grantedBy, new Date()]);
29
12
  }
30
13
  async has(sessionId, toolName) {
31
14
  const [rows] = await this.pool.query("SELECT 1 FROM approval_exemption WHERE session_id = ? AND tool_name = ?", [sessionId, toolName]);
@@ -48,7 +31,6 @@ export class TiDBApprovalExemptionStore {
48
31
  return r.affectedRows;
49
32
  }
50
33
  }
51
- // ─── PG twin (dialect twin of the TiDB store — same contract, $n placeholders + ON CONFLICT) ─────────────
52
34
  export class PgApprovalExemptionStore {
53
35
  pool;
54
36
  constructor(pool) {
@@ -85,7 +67,6 @@ export class PgApprovalExemptionStore {
85
67
  return res.rowCount ?? 0;
86
68
  }
87
69
  }
88
- // ─── Memory twin (tests / ephemeral) ─────────────────────────────────────────────────────────────────────
89
70
  export class MemoryApprovalExemptionStore {
90
71
  bySession = new Map();
91
72
  async grant(sessionId, toolName, grantedBy) {
@@ -129,7 +110,6 @@ export class FileApprovalExemptionStore {
129
110
  pathFor(sessionId) {
130
111
  return join(this.dir, `${sanitizePathComponent(sessionId)}.jsonl`);
131
112
  }
132
- /** Replay every session log (torn-tail-safe; last-wins with revoke tombstones). Index keyed by the SANITIZED id. */
133
113
  hydrate() {
134
114
  let names;
135
115
  try {
@@ -211,13 +191,9 @@ export class FileApprovalExemptionStore {
211
191
  log.close();
212
192
  this.logs.delete(key);
213
193
  }
214
- // 🔴 unlink BEFORE dropping the index, and FAIL LOUD if the file survives: a
215
- // swallowed rm would let hydrate() resurrect purged exemptions after a restart — an E21 right-to-delete
216
- // violation with PRIVILEGE attached (auto-approve rights), unlike the inert anchor-store twin. Throwing
217
- // here aborts the caller's session purge before it deletes the session metadata (fail-loud, retryable).
218
194
  const path = this.pathFor(sessionId);
219
195
  try {
220
- rmSync(path, { force: true }); // force: missing file is fine (idempotent delete)
196
+ rmSync(path, { force: true });
221
197
  }
222
198
  catch (e) {
223
199
  if (existsSync(path))
@@ -226,14 +202,12 @@ export class FileApprovalExemptionStore {
226
202
  this.bySession.delete(key);
227
203
  return removed;
228
204
  }
229
- /** Release every open append-log fd (LocalBackend.close → graceful-restart re-open). */
230
205
  dispose() {
231
206
  for (const log of this.logs.values()) {
232
207
  try {
233
208
  log.close();
234
209
  }
235
210
  catch {
236
- /* best-effort */
237
211
  }
238
212
  }
239
213
  this.logs.clear();