@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,50 +1,8 @@
1
- /**
2
- * Durable backing store for offloaded large tool results (core 1.47 offload / 1.49 durable contract) —
3
- * SINGLE-FILE DUAL-DIALECT (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical
4
- * `TiDBToolResultStore` / `PgToolResultStore` class names survive as thin ctor subclasses so every consumer
5
- * (store-backend.ts, the fake-pool unit suites, the real-DB integration suites) is untouched.
6
- *
7
- * Mirrors core's reference `TiDbToolResultStore` adapter. Without this, the Runner defaults to a
8
- * task-scoped in-memory store: an async run that wakes on ANOTHER replica can't `read_tool_result` an
9
- * offloaded result (the ref misses → the model is told it's unavailable; the preview still stands, so it
10
- * degrades, never crashes). A durable store survives wake/resume across the stateless fleet.
11
- *
12
- * core namespaces the ref as `tr_<sessionId>~<toolCallId>~<contentCoordinate>` (1.49, re-minted injectively in
13
- * 5.26.0 #119 — `~`-separated, four segments max, content-addressed) → globally unique → usable directly as
14
- * the PRIMARY KEY (no composite key needed). `put` is write-once (idempotent on replay/retry: a retry is
15
- * a NEW toolCallId → new ref, so a given ref never changes content). Retention is anchored to a run's
16
- * RECOVERABLE window via TTL (`reapOlderThan`): a resumable run may re-fetch an old ref after wake, but a
17
- * completed run only needs the preview for audit/replay — so a TTL comfortably exceeding the run lifetime
18
- * bounds storage without losing recoverable full-text.
19
- *
20
- * ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
21
- * - `?` placeholders vs `$n`
22
- * - write-once insert = `INSERT IGNORE` vs `INSERT ... ON CONFLICT (ref) DO NOTHING`
23
- * - affectedRows vs rowCount (via SqlDriver)
24
- * - `SUBSTRING(content, ?, ?)` (1-indexed, comma form) vs the ANSI `SUBSTRING(content FROM $n::int FOR
25
- * $m::int)` positional form WITH explicit `::int` casts — node-pg binds bare params as text, and
26
- * `SUBSTRING(s FROM <text>)` is the POSIX-REGEX overload (it would treat $1 as a pattern); the cast
27
- * forces the positional integer overload. `CHAR_LENGTH` is ANSI and shared.
28
- * - content column: TiDB LONGTEXT vs PG TEXT (PG has no LONGTEXT; TEXT is unbounded), DATETIME(3) vs
29
- * TIMESTAMPTZ(3) — schema only (TiDB DDL in tidb-pool.ts; PG DDL self-contained below, exported for the
30
- * integration test + pg-pool.ts's central apply).
31
- * - `put`'s UTF-16 sanitize is a GENUINE algorithm divergence, not just SQL text — see the dialect branch
32
- * inline for why.
33
- * - (历史条目,已随码消失)ESCAPE-clause literal:`deleteBySession` 曾按 ref 前缀 LIKE 选行,两方言因此
34
- * 各自显式声明转义符。重扫二轮起该腿改为**只按 `owner_session_id` 出处选行**(core 契约:ref 是
35
- * opaque handle,禁解析),本文件已无 LIKE,这条方言差随之退役 —— 留档以防有人「照旧例」把前缀
36
- * 选行请回来。
37
- */
38
1
  import { MAX_MINTED_TOOL_RESULT_REF_CHARS, assertSafeToolResultRef, assertToolResultProvenanceMatch, normalizeToolResultProvenance } from "@sema-agent/core";
39
2
  import { pgSanitizeText } from "./pg-safe-json.js";
40
3
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
41
4
  import { MANAGED_RETENTION } from "./retention-store-sql.js";
42
- /** PG schema for the tool_result table — the PG translation of tidb-pool.ts's tool_result DDL
43
- * (LONGTEXT→TEXT, DATETIME(3)→TIMESTAMPTZ(3), inline KEY→separate CREATE INDEX). Disjoint from other stores'
44
- * tables, so a self-contained ensureSchema is safe (central pg-pool aggregation is done separately). */
45
5
  export const PG_TOOL_RESULT_SCHEMA = [
46
- // #119(core 5.26.0):宽度与出处两列与 TiDB 双生逐字对齐 —— 理由写在 tidb-pool.ts 的同一张 DDL 上
47
- // (`MAX_MINTED_TOOL_RESULT_REF_CHARS` = 518;截断主键 = 两枚 ref 折成一行 = 身份故障)。
48
6
  `CREATE TABLE IF NOT EXISTS tool_result (
49
7
  ref VARCHAR(518) COLLATE "C" NOT NULL,
50
8
  content TEXT COLLATE "C" NOT NULL,
@@ -55,33 +13,15 @@ export const PG_TOOL_RESULT_SCHEMA = [
55
13
  )`,
56
14
  `CREATE INDEX IF NOT EXISTS idx_tool_result_created ON tool_result (created_at)`,
57
15
  ];
58
- /** Idempotent schema apply for the tool_result table (for the integration test to call). */
59
16
  export async function ensureSchema(pool) {
60
17
  for (const stmt of PG_TOOL_RESULT_SCHEMA)
61
18
  await pool.query(stmt);
62
19
  }
63
- /**
64
- * #119 升级前置断言 —— **拒启**,不是 warn(codex 复审 [high],已核真)。
65
- *
66
- * 病:本仓不发 `ALTER TABLE` 迁移(标准裁定:改列就改 CREATE + 删库重建)。于是一台**没删表**就升上来的
67
- * 部署,`CREATE TABLE IF NOT EXISTS` 对它是空操作,旧表既没有出处两列也只有 `VARCHAR(190)` 的 ref 列。
68
- * 那样跑起来的后果不是「少个功能」:每一次 offload 写都会撞 unknown column 报错,而 core 的 offload
69
- * 失败臂会把错误吞成一条内联占位("[offload LOST at write time…]")—— 服务照跑,工具产物全丢,日志里只有
70
- * 一句看不出根因的话。这正是「响亮或 fail-closed,禁静默降级」那条要挡的形态。
71
- *
72
- * 判据用**能力探测**而不是版本号/information_schema:发一条恒空的 `WHERE 1=0` 读,列不在就报错。
73
- * 探不通即拒启,错误文案直接给出两条方言的动作(删表重建),不让运维去猜。
74
- */
75
20
  export async function assertToolResultProvenanceSchema(query, dialect) {
76
- // ② 键宽(codex 复审 round2 [high],已核真):只探两列会放过「有出处列、但 ref 仍是 VARCHAR(190)」的
77
- // 半迁移表 —— 而这道断言的文案与 CHANGELOG 都在宣称它护着加宽后的键。截断的主键把两枚不同的 ref 折成
78
- // 同一行 = 身份故障,正是本次升级要消灭的东西,所以宽度必须**真查**,不能靠「列在 ⇒ 表是新的」推断。
79
21
  const widthRow = await query(dialect === "tidb"
80
22
  ? "SELECT CHARACTER_MAXIMUM_LENGTH AS len FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'tool_result' AND column_name = 'ref'"
81
23
  : "SELECT character_maximum_length AS len FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'tool_result' AND column_name = 'ref'").catch(() => ({ rows: [] }));
82
24
  const width = Number(widthRow.rows[0]?.len ?? NaN);
83
- // 查不到宽度(权限受限的 information_schema / 意外的列类型)⇒ **不**据此拒启:那是「没看见」,不是
84
- // 「看见了不合格」。看见了才判,判就判死。
85
25
  if (Number.isFinite(width) && width < MAX_MINTED_TOOL_RESULT_REF_CHARS) {
86
26
  throw new Error(`tool_result.ref is VARCHAR(${width}) but core 5.26.0 mints refs up to ${MAX_MINTED_TOOL_RESULT_REF_CHARS} characters ` +
87
27
  `(#119 made the ref injective: four \`~\`-separated segments). A truncating primary key aliases two distinct refs onto ` +
@@ -103,30 +43,16 @@ export async function assertToolResultProvenanceSchema(query, dialect) {
103
43
  `Underlying probe error: ${err instanceof Error ? err.message : String(err)}`);
104
44
  }
105
45
  }
106
- /** Dual-dialect durable ToolResultStore. See the file header for the dialect-delta ledger. */
107
46
  export class SqlToolResultStore {
108
47
  db;
109
- /** #270 车1:托管留存声明。读法与「为什么 SQL 店答 managed 而三方法的实现体在 `retention-store-sql.ts`」
110
- * 逐字见 {@link MANAGED_RETENTION}(本店的 `tool_result` 行由那只聚合店按期清:随会话树删 + 孤儿腿)。 */
111
48
  retention = MANAGED_RETENTION;
112
49
  constructor(db) {
113
50
  this.db = db;
114
51
  }
115
- /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
116
52
  q(tidb, pg) {
117
53
  return this.db.dialect === "tidb" ? tidb : pg;
118
54
  }
119
- /** D1(试剂盒揪出,RB-266 语义):ref 是主键,而 `deleteBySession` 只认**记录下来的出处**与单射的
120
- * `tr_<sid>~` 前缀(重扫二轮起;旧注写的 `tr_<sid>_%` 前缀清理已作废,理由见该方法顶注)——
121
- * 不合规 ref 的行既进不来、也就谈不上被 session 删除清掉,只能等 TTL。
122
- *
123
- * 🔴 判定**单源**:直调 core 的 `assertSafeToolResultRef`(core 2.8.0 起入公共面)。
124
- * 在此之前这里是那 6 个判别条件的**本地镜像** —— 我在 [2159] 主动交出过这条裂缝:镜像是第二真源,
125
- * core 哪天收紧一格(加 Windows 保留名、长度上限……)我方不会跟着动,而**试剂盒会继续绿**
126
- * (它测的是「拒不拒」,不是「按同一张表拒」)。core 按请求追加了导出,镜像随之删除,裂缝闭合。
127
- * 抛错文案与 core 逐字相同,所以 wire 与既有钉都不变。 */
128
55
  isUnsafeRef(ref) {
129
- // core 只给 assert 形(它就是判定本体);读面要的是谓词,由同一个 assert 派生 —— 仍是单源。
130
56
  try {
131
57
  assertSafeToolResultRef(ref);
132
58
  return false;
@@ -138,49 +64,17 @@ export class SqlToolResultStore {
138
64
  assertSafeRef(ref) {
139
65
  assertSafeToolResultRef(ref);
140
66
  }
141
- /**
142
- * #119(core 5.26.0)—— 出处的**写面**。语义一律取 core 单源(`assertToolResultProvenanceMatch`),
143
- * 本方法只负责把它落到 SQL 上:
144
- * · **写一次选举同时定属主** —— 内容与属主是同一条 INSERT,不存在「内容写进去了属主还没跟上」的窗口
145
- * (core 头注允许 file 双对象后端出现这个窗口,单行后端没有,别自造);
146
- * · 抢输的那一方(IGNORE / DO NOTHING ⇒ affected=0)回读已存属主再判:同属主(或本次无出处)=
147
- * 幂等空转;异属主 = `ToolResultRefConflictError` typed 拒。**绝不静默 keep-first**:ref 是主键,
148
- * 静默空转会让第二位写者拿着自己的 ref 读回第一位的字节;
149
- * · 无出处的行永久 unowned —— 后续带出处的 put 不回填(没有证据的收养),`ownerOf` 继续答 undefined。
150
- */
151
67
  async put(ref, content, provenance) {
152
68
  this.assertSafeRef(ref);
153
- // Sanitize invalid UTF-16 (lone/half surrogates — a JS string CAN hold them, e.g. a binary-ish tool
154
- // output) to U+FFFD so the column accepts the FULL value via a Buffer utf8 round-trip (valid content is
155
- // unchanged). Shared by both dialects.
156
69
  let safe = Buffer.from(content, "utf8").toString("utf8");
157
- // 🔴 Genuine algorithm divergence (not just SQL text): TiDB's `INSERT IGNORE` downgrades an invalid-byte
158
- // rejection to a warning + SILENT TRUNCATION (the utf8 round-trip above is enough — TiDB never sees a NUL
159
- // complaint). PG's `ON CONFLICT DO NOTHING` does NOT paper over an encoding error — it errors — and node-pg's
160
- // TEXT columns additionally reject a bare NUL (u0000) that a lone-surrogate round-trip alone does NOT strip
161
- // (codex H4). `pgSanitizeText` double-scans (NUL + lone surrogate → U+FFFD) so PG never sees either. Never
162
- // "unify" onto one engine's sanitize: TiDB doesn't need the NUL scan, and skipping it on PG reintroduces 22P05.
163
70
  if (this.db.dialect === "pg")
164
71
  safe = pgSanitizeText(safe);
165
- // write-once / keep-first: a replay re-puts the SAME ref+content → IGNORE / DO NOTHING keeps the original row.
166
72
  const owner = provenance !== undefined ? normalizeToolResultProvenance(provenance) : undefined;
167
73
  const insert = async () => (await this.db.query(this.q("INSERT IGNORE INTO tool_result (ref, content, owner_session_id, owner_task_id, created_at) VALUES (?,?,?,?,?)", "INSERT INTO tool_result (ref, content, owner_session_id, owner_task_id, created_at) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (ref) DO NOTHING"), [ref, safe, owner?.sessionId ?? null, owner?.taskId ?? null, new Date()])).affected;
168
- // 🔴 codex 复审(medium,已核真)——**抢输之后那一行可能已经不在了**。选举(INSERT)与观察(SELECT)
169
- // 是两条语句,中间可以插进 TTL reap 或 §0.5 会话清除。那时 `readOwner` 回 `absent`,而 core 的比较器
170
- // 对「缺一侧」是**不下判决**的(设计如此:无出处的行不许被收养)—— 于是 put 会**成功返回**,而库里
171
- // 既没有内容也没有属主,调用方拿着一枚当场读不出来的 ref。
172
- //
173
- // 🔴 处置是**抛**,不是重投(codex 复审两轮的合取解;第一版写的重投被第二轮当场驳回,理由成立):
174
- // 重投会让**删除输**。§0.5 会话清除 DELETE 掉这一行之后,一次重投就把它原地复活 —— 而 core 的部分
175
- // offload 写点是不 await 的,能活过 active-run 围栏,于是被删会话的产物重新落库、留到 TTL 才消失,
176
- // 直接违背 E21 的删除权保证。「少存一份可恢复窗口内的缓存」与「删了又回来」不是同一个量级的代价。
177
- // 抛出去会走到 core 的 offload 失败臂 —— 它把这次卸载记成 "[offload LOST at write time: …]" 并保住
178
- // 内联预览,模型看到的是实话。
179
74
  if ((await insert()) > 0)
180
- return; // 本次赢下选举:内容与属主同一条语句落地
75
+ return;
181
76
  const stored = await this.readOwner(ref);
182
77
  if (stored.present) {
183
- // 抢输(或纯重放):属主判定归 core 的单源比较器 —— 「同属主」在两个 store 里不许有两种含义。
184
78
  assertToolResultProvenanceMatch(ref, stored.owner, owner);
185
79
  return;
186
80
  }
@@ -188,121 +82,52 @@ export class SqlToolResultStore {
188
82
  `(a concurrent TTL reap or session purge deleted it). Nothing was stored — reporting the loss rather than ` +
189
83
  `re-inserting (which would resurrect a purged session's content) or returning a ref that reads back empty.`);
190
84
  }
191
- /** 一次回读同时回答两件事:**行在不在**(reap/purge 窗口的判别位)与**属主是谁**。两件事必须来自同一
192
- * 条 SELECT —— 分两次问会重新引入它要消灭的那个窗口。 */
193
85
  async readOwner(ref) {
194
86
  const { rows } = await this.db.query(this.q("SELECT owner_session_id, owner_task_id FROM tool_result WHERE ref = ?", "SELECT owner_session_id, owner_task_id FROM tool_result WHERE ref = $1"), [ref]);
195
87
  const row = rows[0];
196
88
  if (!row)
197
89
  return { present: false, owner: undefined };
198
- // 🔴 缺席的判别位**只有 SQL NULL**(codex 复审 round2 [medium],已核真):曾经写成
199
- // `sessionId.length === 0` 也算无属主 —— 那会把一个 `sessionId: ""` 的合法(core 的
200
- // `normalizeToolResultProvenance` 不拒空串)出处在往返中悄悄折成 unowned,与内存后端的逐字比较分家;
201
- // `taskId: ""` 折成缺席更糟 —— 授权面会从「task 级」放宽成「整条 session」。字符串一律原样还原。
202
90
  const sessionId = row.owner_session_id;
203
91
  if (typeof sessionId !== "string")
204
- return { present: true, owner: undefined }; // NULL 列 = 行在但无属主
92
+ return { present: true, owner: undefined };
205
93
  const taskId = row.owner_task_id;
206
- // 键**缺席**而不是 present-as-undefined:core 的比较器把「缺 taskId」与「显式 undefined」当同一个值,
207
- // 但调用方(和本仓的 toEqual 钉)看得见键在不在,落库的 NULL 只能还原成缺席形。
208
94
  return { present: true, owner: typeof taskId === "string" ? { sessionId, taskId } : { sessionId } };
209
95
  }
210
- /**
211
- * #119 —— 出处的**读面**。未知 ref 与「存了但无属主」两种情况都答 `undefined`:读面对二者一视同仁
212
- * (fail-closed,谁都没被授权),所以这里也不必把它们分开报。
213
- */
214
96
  async ownerOf(ref) {
215
97
  if (this.isUnsafeRef(ref))
216
- return undefined; // 与 get 同尺:写面拒过的 ref 必不在库,不把驱动层错误漏出去
98
+ return undefined;
217
99
  return (await this.readOwner(ref)).owner;
218
100
  }
219
101
  async get(ref, opts = {}) {
220
- // D1 读面半条(RB-266「the read face degrades instead」):unsafe ref 在读面**降级**返回 undefined
221
- // (该 ref 写入面就被拒 ⇒ 必不在库),不把驱动层错误(PG 22021 对 NUL)漏给调用方。
222
102
  if (this.isUnsafeRef(ref))
223
103
  return undefined;
224
104
  const offset = Math.max(0, Math.floor(opts.offset ?? 0));
225
105
  const hasLimit = opts.limit !== undefined && Number.isFinite(opts.limit);
226
106
  const limit = hasLimit ? Math.max(0, Math.floor(opts.limit)) : 0;
227
- // One round-trip: SUBSTRING is 1-indexed (offset+1); CHAR_LENGTH gives the full (character) length for the
228
- // slice header. With no limit, return the remainder from `offset`.
229
107
  const sql = hasLimit
230
108
  ? this.q("SELECT SUBSTRING(content, ?, ?) AS slice, CHAR_LENGTH(content) AS total FROM tool_result WHERE ref = ?", "SELECT SUBSTRING(content FROM $1::int FOR $2::int) AS slice, CHAR_LENGTH(content) AS total FROM tool_result WHERE ref = $3")
231
109
  : this.q("SELECT SUBSTRING(content, ?) AS slice, CHAR_LENGTH(content) AS total FROM tool_result WHERE ref = ?", "SELECT SUBSTRING(content FROM $1::int) AS slice, CHAR_LENGTH(content) AS total FROM tool_result WHERE ref = $2");
232
110
  const params = hasLimit ? [offset + 1, limit, ref] : [offset + 1, ref];
233
111
  const { rows } = await this.db.query(sql, params);
234
112
  if (rows.length === 0)
235
- return undefined; // unknown ref (e.g. reaped) → core reports "no longer available"
113
+ return undefined;
236
114
  return { content: String(rows[0].slice ?? ""), offset, totalChars: Number(rows[0].total) };
237
115
  }
238
- /** TTL reap: delete results older than `cutoffMs`. Returns rows removed.
239
- * C6: a real DB error must NOT collapse into the same `0` a legitimate "nothing was old enough" returns —
240
- * those are different facts (query failed vs. query succeeded on an empty set) and folding them together
241
- * makes a stuck/broken reaper indistinguishable from a healthy quiet one. Matches every sibling reaper's
242
- * form (roster-store-sql.ts `reapOlderThan`, checkpoint-store-sql.ts `reapExpired`,
243
- * workflow-journal-store-sql.ts `reapExpired`): let the error propagate — the periodic-sweep call site
244
- * (boot/reapers.ts) already wraps this call in `.catch(() => undefined)` so the reap loop itself never dies. */
245
116
  async reapOlderThan(cutoffMs) {
246
117
  const { affected } = await this.db.query(this.q("DELETE FROM tool_result WHERE created_at < ?", "DELETE FROM tool_result WHERE created_at < $1"), [new Date(cutoffMs)]);
247
118
  return affected;
248
119
  }
249
- /**
250
- * E21 (§0.5 session delete) — purge offloaded tool results for one session. Selection is by the row's
251
- * **recorded provenance** (`owner_session_id`, the #119 column `put` writes in the same statement as the
252
- * content) and by NOTHING else — **zero ref-derived arms**(codex R1-[high] 收窄:连单射新前缀也不作
253
- * 属主证据,理由在方法体内逐字引 core 顶注)。Returns `{deleted, unattributable}`; why NO prefix
254
- * (legacy `_` or injective `~`) is a deletion selector is inline below.
255
- *
256
- * ⚠️ 旧注两处已作废(重扫二轮更正,留档防复辟):① 「core namespaces every ref as `tr_<sid>_<callId>`」——
257
- * 5.26.0 起是 `~` 分隔的单射四段形,`_` 形只剩存量;② 「there is no `session_id` (or `owner`) column to
258
- * guard on」—— #119 起**有**(`owner_session_id` / `owner_task_id`,boot 期还有拒启断言把没升级的表挡在
259
- * 外面),所以「按出处选行」不再是「awkward ref-correlated EXISTS」,它就是本方法现在的主选择器。
260
- *
261
- * 路由侧的 owner 门照旧在(DELETE 路由先 `ownerOf` 对属主、同一协调器里先跑 owner-guarded 的 run 账本
262
- * 删除),但它不再是唯一防线:LIKE 转义只挡「精心构造的 id 把前缀撑宽」,挡不住「**合法**的邻会话 id
263
- * 恰好是本会话 id 的扩展」——那一条现在由出处判定挡。
264
- */
265
120
  async deleteBySession(sessionId) {
266
- // 🔴 **选行只按记录下来的出处,一个 ref 也不解析**(重扫二轮红先修 + codex 复审 R1-[high] 收窄)。
267
- // core `tool-result-store.d.ts` 对本操作写死了三条,逐字照做:
268
- // ① 「**selection is by RECORDED PROVENANCE, never by parsing the ref.** A ref is an opaque handle:
269
- // its session segment may be folded, refs minted before the injective form decompose two ways
270
- // (`tr_team_blue_x` is both ("team","blue_x") and ("team_blue","x")), **and a caller may mint its
271
- // own**. Prefix-matching a ref therefore both misses rows and reaches rows of a NEIGHBOURING
272
- // session, and over-deletion here destroys a live session's readable bytes」;
273
- // ② 「**`taskId` is ignored in the match**」——`{sessionId, taskId}` 是同一会话的窄化,不是别的属主;
274
- // ③ 「**an UNOWNED entry is never deleted** … It is counted instead (`unattributable`) so the caller
275
- // learns the deletion was incomplete rather than being told a clean "done"」。
276
- //
277
- // 旧形拿两条 LIKE 前缀全删,踩的正是 ①:提交面对 session id **只限长、不限形**(`server.ts` 的
278
- // "a client MAY supply an ARBITRARY id … LENGTH-ONLY — NOT a uuidv7 shape check"),而 `_` 在 core 的
279
- // `NATIVE_REF_CHARSET`(`/^[A-Za-z0-9_.-]+$/`)里 ⇒ 会话 `<uuid>_sub` 合法,它的产物铸出
280
- // `tr_<uuid>_sub~c1~s<hash>`;删父会话 `<uuid>`(过得了 DELETE 路由的 uuidv7 门)时旧前缀 LIKE 命中它,
281
- // 一条**活着的**邻会话的卸载全文被销毁,还被算进本次的 `deleted`。
282
- // 🔴 **新前缀也不作属主证据**(codex R1-[high],验真后收):`tr_<sid>~` 的单射只保证「core 用这个
283
- // sessionId 铸出来的 ref 长这样」,**不**保证「长这样的 ref 一定是 core 用这个 sessionId 铸的」——
284
- // `put(ref, content)` 的 ref 是 opaque handle、两参形合法、调用方可自铸(core 顶注 ① 逐字点名)。
285
- // 拿它删无出处行 = 又一次「按 ref 猜属主」,只是猜得像一点。
286
- //
287
- // ⇒ `deleted` = `owner_session_id` 命中的行(ref 形状无关,会话 id 触发 `refSegment` 折叠时也不漏);
288
- // `unattributable` = 库里**所有**无出处行的计数(与 core `InMemoryToolResultStore.deleteBySession`
289
- // 逐字同口径:它遍历全表,`provenance === undefined` 的一律 `unattributable++` 且**跳过不删**)。
290
- // 这个数是「本店有多少行没人认领」这一**全库**事实,读法 = 「这次会话删除不可能是完整的」,
291
- // 不是「本会话残留了 N 行」。存量无出处行的实体清理归 TTL(`reapOlderThan`)。
292
- // ⚠️ 登记后续件:`owner_session_id` 上没有索引(本表只有 PK(ref) 与 created_at 索引),删与计数都是
293
- // 全表扫。会话删除是管理面低频动作,先取正确与完整;要提速得改 DDL + 重生成两方言 baseline,那是独立一车。
294
121
  const deletion = await this.db.query(this.q("DELETE FROM tool_result WHERE owner_session_id = ?", "DELETE FROM tool_result WHERE owner_session_id = $1"), [sessionId]);
295
122
  const { rows } = await this.db.query(this.q("SELECT COUNT(*) AS n FROM tool_result WHERE owner_session_id IS NULL", "SELECT COUNT(*) AS n FROM tool_result WHERE owner_session_id IS NULL"));
296
123
  return { deleted: deletion.affected, unattributable: Number(rows[0]?.n ?? 0) };
297
124
  }
298
125
  }
299
- /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
300
126
  export class TiDBToolResultStore extends SqlToolResultStore {
301
127
  constructor(pool) {
302
128
  super(mysqlDriver(pool));
303
129
  }
304
130
  }
305
- /** PostgreSQL binding — historical class name + ctor shape preserved. */
306
131
  export class PgToolResultStore extends SqlToolResultStore {
307
132
  constructor(pool) {
308
133
  super(pgDriver(pool));
@@ -1,7 +1,5 @@
1
1
  import { EMPTY_USAGE_WINDOW_RECORD, chargeUsageRecord, readUsageRecord } from "@sema-agent/core";
2
2
  export const USAGE_WINDOW_TABLE = "usage_window";
3
- /** record 列 → UsageWindowRecord。坏形 fail-loud(治理窗算错=计费面撒谎;与快照/凭据类 store 的
4
- * onCorruptRead 披露座不同族——这里没有「跳过一行继续」的安全语义,账本坏=停)。 */
5
3
  function parseRecord(raw, key) {
6
4
  let v;
7
5
  try {
@@ -14,8 +12,6 @@ function parseRecord(raw, key) {
14
12
  if (typeof v !== "object" || v === null || !Array.isArray(r.slots) || !Array.isArray(r.buckets)) {
15
13
  throw new Error(`usage_window record for key "${key}" has an invalid shape (expected {slots[],buckets[]}) — refusing to mis-count a governance window`);
16
14
  }
17
- // 深度对齐 core FileUsageWindowStore(validateSlots/validateBuckets):内层坏形若放过,非数值
18
- // tokens 参与累加得 "0oops" ⇒ exhausted:false,超额账户被判未耗尽。逐条校验,任一不合即 fail-loud。
19
15
  const badShape = (detail) => new Error(`usage_window record for key "${key}" has an invalid shape (${detail}) — refusing to mis-count a governance window`);
20
16
  const finite = (x) => typeof x === "number" && Number.isFinite(x);
21
17
  const slots = r.slots.map((s) => {
@@ -62,7 +58,6 @@ export class TiDBUsageWindowStore {
62
58
  const c = await this.pool.getConnection();
63
59
  try {
64
60
  await c.beginTransaction();
65
- // ensure-first:幂等建空行(EMPTY record)⇒ 下一步 FOR UPDATE 行恒在(锁语义干净,无 gap-lock 死锁形)。
66
61
  await c.query(`INSERT IGNORE INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES (?, ?, ?)`, [
67
62
  key,
68
63
  JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
@@ -70,7 +65,6 @@ export class TiDBUsageWindowStore {
70
65
  ]);
71
66
  const [rows] = (await c.query(`SELECT record FROM ${USAGE_WINDOW_TABLE} WHERE usage_key = ? FOR UPDATE`, [key]));
72
67
  if (rows.length === 0) {
73
- // 本表无删除臂(不同于 mailbox 的 drop/reap),ensure 后空 = 真障碍 fail-loud。
74
68
  throw new Error(`usage_window row for key "${key}" vanished between ensure and lock — refusing to drop a governance charge`);
75
69
  }
76
70
  const record = parseRecord(rows[0].record, key);
@@ -101,8 +95,6 @@ export class PgUsageWindowStore {
101
95
  const c = await this.pool.connect();
102
96
  try {
103
97
  await c.query("BEGIN");
104
- // ensure-first(TiDB 同注):ON CONFLICT DO NOTHING 幂等建空行,再锁——插入不带锁,锁只从
105
- // FOR UPDATE 来(复审靶1 的 PG 交错:并发首充双双 ensure,后到方 DO NOTHING 后必须真锁行)。
106
98
  await c.query(`INSERT INTO ${USAGE_WINDOW_TABLE} (usage_key, record, updated_at_ms) VALUES ($1, $2, $3) ON CONFLICT (usage_key) DO NOTHING`, [
107
99
  key,
108
100
  JSON.stringify(EMPTY_USAGE_WINDOW_RECORD),
@@ -1,63 +1,8 @@
1
- /**
2
- * design/102 (K-8 CC full-body) — the WebSearch BACKEND (BRAIN / model-service leg) that core leaves
3
- * deployment-injected (`WebSearchConfig.search`; core ships NONE, the same boundary as the model gateway). Wiring a
4
- * backend here is what makes `assembleFullBodyTools({ webSearch })` actually mount the WebSearch tool.
5
- *
6
- * Multi-provider, picked by `WEB_SEARCH_PROVIDER`:
7
- * - `brave` — Brave Web Search API (GET, `X-Subscription-Token` header)
8
- * - `tavily` — Tavily Search API (POST, `Authorization: Bearer`, native include/exclude_domains)
9
- * - `searxng` — a self-hosted SearXNG instance (GET `?format=json`, no auth)
10
- *
11
- * Each returns `{ title, url, snippet }[]`; core does the rest — it `delimitUntrusted`-fences the (UNTRUSTED) results
12
- * and re-enforces the `allowed_domains`/`blocked_domains` FLOOR on them, so honoring `opts` here is an optimization,
13
- * not a correctness requirement (a backend MAY ignore it). The API key comes from deployment env — it never enters
14
- * the model prompt or the tool args. `effect:"read"` (idempotent) is core's concern; this is just the transport.
15
- *
16
- * ── 立案:装配层四条缺口([2176] 我方认领,2026-08-01 调研,**尚未实施**)────────────────────
17
- *
18
- * ① **`opts` 仍有 brave 腿不消费**(只剩 `brave` 的 switch 分支不传 `opts`;`searxng` 腿已随 ② 的
19
- * 还债改动接住 `opts` 并透传给 core 的 adapter,由它把 `allowedDomains` 原生下推成 `site:` 前缀。
20
- * 立案时的原话是「`opts` 只有 tavily 腿在消费(`brave`/`searxng` 都不传)」,② 落地后已部分还清)。
21
- * 定性:**优化缺失,不是正确性缺口** —— 上面那句「core 再执行一次 FLOOR」经亲验属实
22
- * (core `dist/tools/web.js` 的 `webSearchResultAllowed(url, allowed, blocked)`,按 hostname
23
- * 逐条过滤 blocked/allowed)。所以模型请求的域限制**不会**被静默丢弃。
24
- * 但代价真实:后端搜回一堆注定被 core 丢掉的结果 ⇒ 白花配额、白等延迟,极端情况下
25
- * 「限域搜索」返回 0 条(拿回来的 N 条全不在 allowed 里)而后端其实能做原生过滤。
26
- * ⚠️ 判据留痕:我第一轮把这条判成了「约束被静默丢弃」的安全缺口,是**亲读 core 结果侧代码**
27
- * 才推翻的。「别处已经处理了」这种声明必须验证 —— 这次它是真的,但真假只能靠读。
28
- *
29
- * ② ~~**searxng 腿是自铸的第二真源**~~ —— ✅ **已还(2026-08-01,[2229] 裁定 searxng 为国内可达性
30
- * 主路线之后)**:本腿改为消费 core 的 `createSearxngSearchBackend`,外层保住 maxResults 截断 /
31
- * 统一形状 / 坏 payload 观测器三样(各有红先钉 + 变异实测),白得 `site:` 原生下推、`extraParams`、
32
- * 自带 timeout。以下为原始立案文本,留档:
33
- * core 2.10 起已导出 `createSearxngSearchBackend`
34
- * (+`SearxngBackendOptions`),而且比本文件的实现多三样:`allowedDomains` 拼 `site:` 原生下推、
35
- * `extraParams`(实例特定的 `engines=`/`language=` 等)、**自带 timeout**
36
- * (`AbortSignal.any([signal, timeout])`;本文件的 searxng 腿只吃外部 signal)。
37
- * 我在黑板 [2190] 已公开表态「按 core 的接、不再自铸第二真源」—— 这条是待兑现的债。
38
- * 换的时候要核对的等价点:本文件的 `normalize()` 统一形状 + `max` 截断 +
39
- * `setWebSearchBadPayloadObserver` 观测器,core 的 adapter 都没有,得在外层补回来。
40
- *
41
- * ③ **四级优先级语义未定**(env / config-center / per-scenario / per-request 谁压谁)。
42
- * 🔴 三问里「谁被伤」这条决定了默认值:一个**只配了模型 key** 的既有部署,若新语义让某一级
43
- * 能自行打开搜索,它就在升级后**凭空长出一条出网工具** —— 那是安全姿态变更,不是特性。
44
- * ⇒ 语义定稿前,任何新的开启通道**默认 OFF**;开启必须是部署方的显式动作。
45
- *
46
- * ④ ~~**探活 verb 缺席**~~ —— ✅ **已还(2026-08-01)**:`shouldProbeWebSearchOnBoot` 门控 +
47
- * `main.ts` 消费 core 的 `probeSearchBackend`。**默认 OFF**(开着=给每个既有部署的每次启动凭空
48
- * 加一次出网)、**只认明确真值**(含糊值当没开,别替部署方猜)、**失败只 warn 不拒启**(功能型
49
- * 能力缺席 ⇒ 降级)、**不 await**(拖住 boot 就把可选工具变成启动依赖)。以下为原始立案文本,留档:
50
- * core 2.10 起导出 `probeSearchBackend(search, {timeoutMs}) →
51
- * {ok:true,results:n} | {ok:false,error}`,正好填这条:部署方现在只能靠「跑一个真任务」
52
- * 验证搜索配对不对。挂哪儿(boot 期一次性 warn / `/health` 子字段 / 显式端点)未定 ——
53
- * 注意 boot 期探活会给每次启动加一次出网,多租/离线部署要能关。
54
- */
55
1
  import { createSearxngSearchBackend } from "@sema-agent/core";
56
2
  const DEFAULT_MAX_RESULTS = 10;
57
3
  const DEFAULT_TIMEOUT_MS = 10_000;
58
4
  const BRAVE_BASE = "https://api.search.brave.com/res/v1/web/search";
59
5
  const TAVILY_BASE = "https://api.tavily.com/search";
60
- /** Combine the caller's AbortSignal with a per-search timeout into one signal; `done()` clears the timer + listener. */
61
6
  function withTimeout(signal, ms) {
62
7
  const ctrl = new AbortController();
63
8
  const onAbort = () => ctrl.abort();
@@ -74,13 +19,6 @@ function withTimeout(signal, ms) {
74
19
  async function errorText(res) {
75
20
  return (await res.text().catch(() => "")).slice(0, 200);
76
21
  }
77
- /**
78
- * Normalize an UNTRUSTED provider results payload: guard a non-array (a misconfigured proxy / buggy self-hosted
79
- * SearXNG could return a non-list or null elements), skip null/non-object rows, coerce every field to a string, drop
80
- * url-less rows, cap to max. `pick` maps each provider's row to the common shape (Brave→description, others→content).
81
- */
82
- /** LOW (SILENT-FALLBACK P1): a provider returning a non-array `results` used to silently become zero results
83
- * for the model. main.ts wires this to web_search_bad_payload_total{provider} + a warn. */
84
22
  let onBadPayload;
85
23
  export function setWebSearchBadPayloadObserver(fn) {
86
24
  onBadPayload = fn;
@@ -91,18 +29,18 @@ function normalize(raw, pick, max, provider) {
91
29
  try {
92
30
  onBadPayload?.(provider);
93
31
  }
94
- catch { /* observability never breaks search */ }
32
+ catch { }
95
33
  }
96
- return []; // absent field (undefined/null) = legitimately empty; a PRESENT non-array = malformed payload
97
- } // 🔴 review LOW: a non-array `results` must not throw a raw TypeError upstream
34
+ return [];
35
+ }
98
36
  const out = [];
99
37
  for (const row of raw) {
100
38
  if (!row || typeof row !== "object")
101
- continue; // a null / non-object element is not a result
39
+ continue;
102
40
  const f = pick(row);
103
41
  const url = typeof f.url === "string" ? f.url : "";
104
42
  if (!url)
105
- continue; // no URL ⇒ useless to the model (and to core's domain re-enforcement)
43
+ continue;
106
44
  out.push({ title: typeof f.title === "string" ? f.title : "", url, snippet: typeof f.snippet === "string" ? f.snippet : "" });
107
45
  if (out.length >= max)
108
46
  break;
@@ -114,8 +52,7 @@ async function braveSearch(fetchImpl, cfg, query, max, signal) {
114
52
  throw new Error("WEB_SEARCH_API_KEY is required for the brave provider");
115
53
  const url = new URL(cfg.endpoint || BRAVE_BASE);
116
54
  url.searchParams.set("q", query);
117
- url.searchParams.set("count", String(Math.min(20, max))); // Brave caps count at 20
118
- // Brave has no native include/exclude_domains param — core re-enforces the domain floor on the results.
55
+ url.searchParams.set("count", String(Math.min(20, max)));
119
56
  const res = await fetchImpl(url, { headers: { Accept: "application/json", "X-Subscription-Token": cfg.apiKey }, signal });
120
57
  if (!res.ok)
121
58
  throw new Error(`brave search failed (${res.status}): ${await errorText(res)}`);
@@ -127,7 +64,7 @@ async function tavilySearch(fetchImpl, cfg, query, max, signal, opts) {
127
64
  throw new Error("WEB_SEARCH_API_KEY is required for the tavily provider");
128
65
  const body = { query, max_results: Math.min(20, max) };
129
66
  if (opts?.allowedDomains?.length)
130
- body.include_domains = opts.allowedDomains.slice(0, 300); // Tavily native domain filter
67
+ body.include_domains = opts.allowedDomains.slice(0, 300);
131
68
  if (opts?.blockedDomains?.length)
132
69
  body.exclude_domains = opts.blockedDomains.slice(0, 150);
133
70
  const res = await fetchImpl(cfg.endpoint || TAVILY_BASE, {
@@ -141,27 +78,9 @@ async function tavilySearch(fetchImpl, cfg, query, max, signal, opts) {
141
78
  const data = (await res.json());
142
79
  return normalize(data.results, (r) => ({ title: r.title, url: r.url, snippet: r.content }), max, "tavily");
143
80
  }
144
- /**
145
- * SearXNG 腿 —— **消费 core 的 `createSearxngSearchBackend`,不再自铸**(#81② 还债;[2190] 表态、
146
- * [2229] 裁定 searxng 是国内可达性主路线之后该还的那笔)。
147
- *
148
- * core 的 adapter 比本文件此前的手写实现多三样,换过来就白得:
149
- * · `allowedDomains` **原生下推**成 `site:a OR site:b` 前缀(此前本腿完全忽略 opts —— core 工具层
150
- * 有结果侧 FLOOR 兜底所以不是正确性缺口,但白搜一趟、白花配额,极端情况下限域搜索回 0 条);
151
- * · `extraParams`(实例特定的 `engines=` / `language=` 等)——一键装 SearXNG 的向导要用;
152
- * · **自带 timeout**(`AbortSignal.any([signal, timeout])`;此前本腿只吃外部 signal)。
153
- *
154
- * ⚠️ core adapter **不管**这三样,必须留在外层(换真源最容易丢的就是它们,已上红先钉):
155
- * ① `maxResults` 截断 ② 统一形状 `{title,url,snippet}` ③ 坏 payload 观测器。
156
- */
157
81
  async function searxngSearch(fetchImpl, cfg, query, max, signal, timeoutMs, opts) {
158
82
  if (!cfg.endpoint)
159
83
  throw new Error("WEB_SEARCH_ENDPOINT (the SearXNG instance URL) is required for the searxng provider");
160
- // 🔴 timeoutMs 必须显式传:core 的 adapter 内部是 `options.timeoutMs ?? 10_000`,不传就用它自己的
161
- // 10 秒。外层 createWebSearchBackend 那道 withTimeout 只在**更短**时先到 —— 部署方把
162
- // WEB_SEARCH_TIMEOUT_MS 设成大于 10s 的值(慢实例/自建 SearXNG 的常见需要)会被**静默封顶在 10s**,
163
- // 而 3.18.0 的 CHANGELOG 还把 core「自带 timeout」当纯收益卖(见 3.20.0 的更正段)。
164
- // 两层同值不冲突:内层是 adapter 自己的 AbortSignal.any,外层是本文件的 withTimeout。
165
84
  const search = createSearxngSearchBackend(cfg.endpoint, {
166
85
  fetchImpl,
167
86
  timeoutMs,
@@ -172,25 +91,18 @@ async function searxngSearch(fetchImpl, cfg, query, max, signal, timeoutMs, opts
172
91
  rows = await search(query, signal, opts);
173
92
  }
174
93
  catch (e) {
175
- // core 对「results 不是数组」抛具名错;本腿的既有契约是**不抛给上层、走观测器**(review LOW:
176
- // 畸形 payload 不该变成裸 TypeError 上抛)。保持既有契约,把它翻译回观测器 + 空结果。
177
94
  const msg = e instanceof Error ? e.message : String(e);
178
95
  if (/no results array/i.test(msg)) {
179
96
  try {
180
97
  onBadPayload?.("searxng");
181
98
  }
182
- catch { /* observability never breaks search */ }
99
+ catch { }
183
100
  return [];
184
101
  }
185
102
  throw e;
186
103
  }
187
- // core 已回 `{title,url,snippet}` 形;仍过一遍 normalize —— ① 截断 ② 形状兜底 ③ 观测器同源。
188
104
  return normalize(rows, (r) => ({ title: r.title, url: r.url, snippet: r.snippet }), max, "searxng");
189
105
  }
190
- /**
191
- * Build the `WebSearchConfig` (a `search` fn + `maxResults`) for `assembleFullBodyTools({ webSearch })`. The API key
192
- * stays captured in this closure — it is never surfaced to the model or the tool args.
193
- */
194
106
  export function createWebSearchBackend(cfg) {
195
107
  const fetchImpl = cfg.fetchImpl ?? fetch;
196
108
  const maxResults = Math.max(1, Math.min(20, cfg.maxResults ?? DEFAULT_MAX_RESULTS));
@@ -213,22 +125,8 @@ export function createWebSearchBackend(cfg) {
213
125
  };
214
126
  return { search, maxResults };
215
127
  }
216
- /**
217
- * `WEB_SEARCH_SEARXNG_PARAMS` / `settings.webSearch.searxngParams` 的解析 —— SearXNG 实例侧参数
218
- * (`engines=` / `language=` 等)。
219
- *
220
- * 🔴 [2240] cli 逐串直证:3.18.0 我加了 `searxngParams` 字段和消费点,**却没开任何配置入口** ——
221
- * 两条门都没有位置填它,于是我在 [2239] 说的「这条是给你们的」是句空话。单测直接构造
222
- * `WebSearchBackendConfig` 传进去,**天然绕过了配置入口这一层**,于是「能力在」与「配得进」之间
223
- * 断开,而两边的绿都是真的。判据:**按消费端实际能填的那个入口验,不是按自己构造的对象验。**
224
- *
225
- * 形状:`k=v` 用 `;` 分隔(`engines=bing,duckduckgo;language=zh-CN`)——值里本来就常含逗号,
226
- * 所以分隔符取 `;` 而不是 `,`。整串解析不出任何一对 ⇒ **返回 undefined,键整个不铸**
227
- * (不产出空对象:空对象会让下游以为「配了但是空的」,与本仓「缺席就要缺席得干净」同源)。
228
- */
229
128
  export function parseSearxngParams(raw) {
230
129
  if (typeof raw === "object" && raw !== null) {
231
- // settings 门传的是已解析的表;只收「字符串→字符串」,任一值非字符串 ⇒ 整表不铸(不半吞)。
232
130
  const entries = Object.entries(raw);
233
131
  if (entries.length === 0 || entries.some(([, v]) => typeof v !== "string"))
234
132
  return undefined;
@@ -248,44 +146,26 @@ export function parseSearxngParams(raw) {
248
146
  }
249
147
  return Object.keys(out).length > 0 ? out : undefined;
250
148
  }
251
- /**
252
- * #81④ 探活门控 —— **默认 OFF**,显式开才探。
253
- *
254
- * 为什么默认 OFF:探活是一次**真出网**。开着就等于给每个既有部署的每次启动凭空加一次外呼 ——
255
- * 多租/离线部署不能被这样动。这与 #81③「任何新的开启通道默认 OFF」同源:
256
- * 一个只配了模型 key 的部署,不该因为升级而多出网络行为。
257
- *
258
- * 为什么只认明确真值:`"yes"`/`"on"` 这类**含糊值当没开**。含糊值上放行 = 替部署方猜意图,
259
- * 而猜错的方向是「凭空出网」。缺席要缺席得干净,含糊也一样。
260
- */
261
149
  export function shouldProbeWebSearchOnBoot(env = process.env) {
262
150
  const raw = env.WEB_SEARCH_PROBE_ON_BOOT?.trim().toLowerCase();
263
151
  return raw === "true" || raw === "1";
264
152
  }
265
- /** Parse the deployment env into a backend config; `undefined` when no provider is set (⇒ WebSearch not assembled). */
266
153
  export function webSearchConfigFromEnv(env = process.env) {
267
154
  const provider = env.WEB_SEARCH_PROVIDER?.trim().toLowerCase();
268
155
  if (provider !== "brave" && provider !== "tavily" && provider !== "searxng")
269
156
  return undefined;
270
- const searxngParams = parseSearxngParams(env.WEB_SEARCH_SEARXNG_PARAMS); // [2240] 部署 env 门
157
+ const searxngParams = parseSearxngParams(env.WEB_SEARCH_SEARXNG_PARAMS);
271
158
  const maxRaw = Number(env.WEB_SEARCH_MAX_RESULTS ?? "");
272
159
  const timeoutRaw = Number(env.WEB_SEARCH_TIMEOUT_MS ?? "");
273
160
  return {
274
161
  provider,
275
162
  ...(env.WEB_SEARCH_API_KEY ? { apiKey: env.WEB_SEARCH_API_KEY } : {}),
276
163
  ...(env.WEB_SEARCH_ENDPOINT ? { endpoint: env.WEB_SEARCH_ENDPOINT } : {}),
277
- ...(searxngParams ? { searxngParams } : {}), // [2240] 解析不出任何一对 ⇒ 键整个不铸(不产出空对象)
164
+ ...(searxngParams ? { searxngParams } : {}),
278
165
  ...(Number.isFinite(maxRaw) && maxRaw > 0 ? { maxResults: Math.floor(maxRaw) } : {}),
279
166
  ...(Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? { timeoutMs: Math.floor(timeoutRaw) } : {}),
280
167
  };
281
168
  }
282
- /**
283
- * Parse an UNTRUSTED per-request `body.settings.webSearch` (the user's shell settings — `{provider, apiKey?, endpoint?,
284
- * maxResults?}`) into a backend config. Returns `undefined` for a missing/invalid provider — NEVER throws, so a bad
285
- * per-request config falls back to the deployment-env backend rather than failing the task. 🔒 The CALLER gates this to
286
- * the single-user host lane (a per-request `endpoint`/`apiKey` is a capability config; on multi-tenant a tenant could
287
- * point `searxng` at an internal URL = SSRF, so multi-tenant uses ONLY the deployment-env backend — design/107 axis).
288
- */
289
169
  export function webSearchConfigFromSettings(raw) {
290
170
  if (!raw || typeof raw !== "object")
291
171
  return undefined;
@@ -298,8 +178,6 @@ export function webSearchConfigFromSettings(raw) {
298
178
  provider,
299
179
  ...(typeof s.apiKey === "string" && s.apiKey ? { apiKey: s.apiKey } : {}),
300
180
  ...(typeof s.endpoint === "string" && s.endpoint ? { endpoint: s.endpoint } : {}),
301
- // [2240]:与 endpoint **同门同辖域** —— 安全边界不在字段粒度,而在调用方把整条门 gate 到
302
- // 单用户 host lane(见本函数头注的 SSRF 段)。单独把它挡在门外是个没有理由的例外。
303
181
  ...(() => {
304
182
  const p = parseSearxngParams(s.searxngParams);
305
183
  return p ? { searxngParams: p } : {};