@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,103 +1,27 @@
1
- /**
2
- * Durable `FileSnapshotStore` (E19 rewind-files) — SINGLE-FILE DUAL-DIALECT (design/158 A12 定型半场).
3
- * ONE implementation, TWO dialects; the historical `TiDBFileSnapshotStore` / `PgFileSnapshotStore` class
4
- * names survive as thin ctor subclasses so every consumer (store-backend.ts, the migration script, the
5
- * contract/integration/minio-equivalence suites) is untouched.
6
- *
7
- * Cross-replica working-tree snapshots keyed by `(scope=sessionId, key=SessionTreeEntry.id)`, the SAME
8
- * entryId anchor as E18 resume-at. Used for (a) rewind (restore a PAST turn's tree in the same env) and
9
- * (b) the seamless local↔cloud work-artifact transfer (2c: a snapshot synced to the cloud, restored in the
10
- * other environment — conversation + files in lockstep).
11
- *
12
- * The env-operating + security-critical half (the tree walk + the symlink-neutralizing restore
13
- * convergence) is core's `captureManifest`/`applyManifest` — REUSED verbatim (re-implementing risks
14
- * re-introducing the symlink-escape / fail-closed bugs core's dual-review fixed). This store provides ONLY
15
- * the durable persistence: content-addressed blobs (sha256 → bytes, deduped across all snapshots, via the
16
- * shared {@link BlobBackend} seam — SQL by default, MinIO when configured) + a per-`(scope,key)` manifest
17
- * (relPath → blobHash) that stays in SQL either way (`snapshot_manifest`).
18
- *
19
- * ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
20
- * - `?` placeholders vs `$n` on every `snapshot_manifest` statement (exists-check, insert, selects, the
21
- * scoped/keyed deletes) — every statement stays written out at the call site via `q()`, never a single
22
- * text with a numbering loop.
23
- * - result unwrapping (`rows`/`affected`) and transaction control (mysql2 native begin/commit/rollback vs
24
- * PG statement form) go through the shared `SqlDriver`/`SqlTxConn` seam (sql-driver.ts) — same
25
- * normalization the other A12 collapses use; NOT a store-specific concern.
26
- * - the default {@link BlobBackend} (`SqlBlobBackend`) and `blobSizes` (`sqlBlobSizes`) take the RAW pool
27
- * + a `"tidb"|"pg"` dialect string directly (blob-backend.ts predates/sits outside this seam — it is a
28
- * sibling module, not part of this collapse), so this store keeps BOTH the `SqlDriver` (for its own
29
- * `snapshot_manifest` statements) and the raw native pool (to hand to blob-backend.ts unchanged).
30
- * - in-flight guard key separator: the TiDB twin used `\x00`, the PG twin used a plain space — NOT a
31
- * dialect requirement (this key never leaves the process or touches SQL), just accidental drift.
32
- * Harmonized to `\x00` (can't collide with a normal scope/key string) — zero observable behavior change
33
- * either way, since the key only dedups concurrent calls on THIS replica within THIS process.
34
- * - `gcOrphanBlobs`'s orphan-SET query text is IDENTICAL for both dialects (no placeholders) — kept as one
35
- * shared statement, not a `q()` pair.
36
- */
37
1
  import { captureManifest, applyManifest, DEFAULT_SNAPSHOT_BOUNDS } from "@sema-agent/core";
38
2
  import { createHash } from "node:crypto";
39
3
  import { SqlBlobBackend, sqlBlobSizes } from "./blob-backend.js";
40
4
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
41
- /** Dual-dialect durable FileSnapshotStore. See the file header for the dialect-delta ledger. */
42
5
  export class SqlFileSnapshotStore {
43
6
  db;
44
7
  pool;
45
8
  bounds;
46
- /** In-flight create-once guard (per process) — mirrors InMemoryFileSnapshotStore: a concurrent 2nd
47
- * snapshot/import of the same (scope,key) on THIS replica is a no-op, not a re-capture. CROSS-REPLICA dup
48
- * is HARMLESS here: the key is an immutable SessionTreeEntry.id (one settled turn → one fixed tree), so
49
- * two replicas capture the SAME relPath→hash rows → restore's Map dedups them identically. (A
50
- * snapshot_header(scope,snap_key) claim row would serialize one winner + drop dup rows — a follow-on
51
- * hardening for if same-session-multi-replica ever happens; v1 is single-replica affinity so it doesn't.) */
52
9
  inFlight = new Set();
53
- /** The content-addressed BYTE store (clay 2026-06-26 object-store offload). DEFAULT = the SqlBlobBackend
54
- * over THIS pool — so a MINIO-unset deployment is byte-identical to before this refactor (the
55
- * `snapshot_blob` table). When MinIO is configured the StoreBackend passes a MinioBlobBackend, moving ONLY
56
- * the heavy bytes off-DB; the MANIFEST (snapshot_manifest) + the orphan-reference computation STAY in SQL. */
57
10
  blobs;
58
- /** [805]③a/[806]④: core 1.287's captureManifest counts enumerate→read-window vanished files it TOLERATED
59
- * (E19 fix — skipped, not failed). The count rides ONLY the captureManifest return value; without this
60
- * sink it is silently dropped. Optional observer set by main.ts (metrics face) — never throws into the
61
- * seam. codex F8: the type admits an ASYNC sink (`void | Promise<void>`) — the call site swallows a
62
- * rejected promise too (a bare try/catch only caught sync throws, so an async observer's rejection
63
- * surfaced as an unhandledRejection and could pierce the never-throw posture process-wide). */
64
- onSkippedVanished = undefined; // explicit initializer: the key must EXIST on the instance (main.ts feature-detects via `in`)
65
- constructor(db,
66
- /** The RAW native pool — handed to blob-backend.ts (SqlBlobBackend / sqlBlobSizes), which sits OUTSIDE
67
- * the SqlDriver seam and dialect-switches on its own `"tidb"|"pg"` string. Not used for this class's OWN
68
- * `snapshot_manifest` statements (those go through `db`). */
69
- pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
11
+ onSkippedVanished = undefined;
12
+ constructor(db, pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
70
13
  this.db = db;
71
14
  this.pool = pool;
72
15
  this.bounds = bounds;
73
16
  this.blobs = blobBackend ?? new SqlBlobBackend(this.db.dialect, this.pool);
74
17
  }
75
- /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
76
18
  q(tidb, pg) {
77
19
  return this.db.dialect === "tidb" ? tidb : pg;
78
20
  }
79
- /** THE ONE content-addressed blob write — the SINGLE source `snapshot()`, `importManifest()`, and the
80
- * `putBlob` seam all funnel through (no drift across the three writers). Routes the bytes through the
81
- * BlobBackend (SQL default or MinIO); the SqlBlobBackend's upsert REFRESHES `created_at` even on a dedup
82
- * hit so a re-used/just-written blob is "touched=now" and GC-grace-protected until a manifest row commits
83
- * (closes the delete-in-use race). */
84
21
  async putBlobRow(hash, bytes) {
85
22
  await this.blobs.putBlob(hash, bytes);
86
23
  }
87
- /** 2c session-sync PUSH side — upload ONE content-addressed blob to the cloud's snapshot store AHEAD of a
88
- * bundle import (§5 streamed blob negotiation: the local peer PUTs only the blobs the cloud is missing,
89
- * then commits the bundle via importSession, whose getBlob reads the cloud's OWN store). A SERVICE method
90
- * (NOT on core's FileSnapshotStore interface — core was asked, non-blocking; the route feature-detects
91
- * `typeof fs.putBlob`). Reuses {@link putBlobRow} VERBATIM (the SAME created_at-refreshing upsert
92
- * importManifest uses), so a PUT and a concurrent reap can't race: the just-PUT blob's created_at=now
93
- * keeps it inside the GC grace window until the import references it from a manifest. Idempotent
94
- * (content-addressed). The route verifies sha256(bytes)===hash BEFORE this. core 1.142.0 made this a
95
- * fail-closed never-throw seam (Promise<FileSnapshotResult>): a BlobBackend write error (e.g. a
96
- * MinioBlobBackend non-2xx PUT) is caught + encoded as `restore_failed`, NEVER thrown out of the seam. */
97
24
  async putBlob(hash, bytes) {
98
- // M3(接入审计 2026-07-08):sha256 自校验是契约的【store 级】强制不变量("VERIFIES content-address
99
- // integrity … a mismatch → read_failed, nothing stored")——不外包给调用方(HTTP 路由前置校验保留=双保险;
100
- // 绕过路由的内部新调用方不再能用错配 hash 污染 content-addressed store)。镜像 core InMemory 参考实现。
101
25
  if (createHash("sha256").update(bytes).digest("hex") !== hash) {
102
26
  return { ok: false, error: { code: "read_failed", message: `putBlob ${hash} fails content-address integrity — nothing stored` } };
103
27
  }
@@ -106,35 +30,20 @@ export class SqlFileSnapshotStore {
106
30
  return { ok: true };
107
31
  }
108
32
  catch (err) {
109
- // A WRITE/upload failure. core's FileSnapshotError code enum has no dedicated write code; `restore_failed` is the
110
- // code core's OWN File backend uses for a putBlob PERSIST failure (vs `read_failed` = content-address integrity),
111
- // so we match it + make the MESSAGE explicit that this is a write/upload failure (finding 19).
112
33
  return { ok: false, error: { code: "restore_failed", message: `putBlob ${hash} write/upload failed: ${err.message}` } };
113
34
  }
114
35
  }
115
- /** CREATE-ONCE: an entryId's tree is immutable, so a 2nd snapshot for an existing key is a no-op {ok:true}. */
116
36
  async snapshot(scope, key, env, root, signal) {
117
- const flightKey = `${scope}\x00${key}`; // harmonized separator — see file header (not a dialect concern)
37
+ const flightKey = `${scope}\x00${key}`;
118
38
  if (this.inFlight.has(flightKey))
119
39
  return { ok: true };
120
40
  this.inFlight.add(flightKey);
121
41
  try {
122
- // M4(接入审计):create-once 前置 has() 探针必须在 try 【内】——DB 瞬态错(连接池抖动)曾从这里
123
- // reject 逃逸 never-throw seam,击穿 core 引擎的裸 await。
124
42
  if (await this.has(scope, key))
125
43
  return { ok: true };
126
- // captureManifest walks `root` via the env, hands each file's bytes to putBlob (content-addressed dedup), and
127
- // returns relPath→hash. Fail-CLOSED: on an enumerate/read/bound error we DON'T persist a partial manifest.
128
- // NEVER-THROW: captureManifest invokes the putBlob callback UN-try/caught, so a throwing BlobBackend (a MinIO
129
- // non-2xx PUT / network blip — far likelier than a DB error) would escape this never-throw seam. Wrap the whole
130
- // capture: a putBlob/MinIO throw → {ok:false, restore_failed}, the contract every caller (E19 auto-snapshot, the
131
- // rewind/2c path) relies on (findings 4/9). The already-PUT distinct blobs are orphans (no manifest references
132
- // them) → collected by the async sweepOrphanBlobs reaper.
133
44
  let cap;
134
45
  try {
135
46
  cap = await captureManifest(env, root, this.bounds, signal, async (hash, bytes) => {
136
- // The shared content-addressed blob write (created_at-refreshing upsert) — so a re-used old blob this in-flight
137
- // snapshot needs is "touched" to now and protected by the GC grace window until its manifest row commits.
138
47
  await this.putBlobRow(hash, bytes);
139
48
  });
140
49
  }
@@ -143,21 +52,16 @@ export class SqlFileSnapshotStore {
143
52
  }
144
53
  if (!cap.ok)
145
54
  return { ok: false, error: cap.error };
146
- // Observability tail ([805]③a): surface tolerated-vanished counts (metric sink; observer errors swallowed —
147
- // an observer must never break the never-throw snapshot seam).
148
55
  if (cap.skippedVanished > 0 && this.onSkippedVanished) {
149
- // codex F8: Promise.resolve(...) absorbs an ASYNC sink's rejection (else it's an unhandledRejection);
150
- // the outer try still absorbs a SYNC throw. Observer errors never break the never-throw seam either way.
151
56
  try {
152
57
  void Promise.resolve(this.onSkippedVanished(cap.skippedVanished)).catch(() => { });
153
58
  }
154
- catch { /* observer must not break the seam */ }
59
+ catch { }
155
60
  }
156
- // Persist the manifest atomically (re-check has() under the txn so a concurrent winner isn't duplicated).
157
61
  await this.tx(async (conn) => {
158
62
  const exists = await conn.query(this.q("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", "SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1"), [scope, key]);
159
63
  if (exists.rows[0])
160
- return; // a concurrent snapshot won — create-once
64
+ return;
161
65
  for (const [relPath, hash] of cap.value) {
162
66
  await conn.query(this.q("INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES (?,?,?,?)", "INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES ($1,$2,$3,$4)"), [scope, key, relPath, hash]);
163
67
  }
@@ -165,49 +69,21 @@ export class SqlFileSnapshotStore {
165
69
  return { ok: true };
166
70
  }
167
71
  catch (err) {
168
- // The manifest-persist tx (or has()) raised — encode it (never throw out of the seam), parity with importManifest.
169
72
  return { ok: false, error: { code: "restore_failed", message: `snapshot persist failed: ${err.message}` } };
170
73
  }
171
74
  finally {
172
75
  this.inFlight.delete(flightKey);
173
76
  }
174
77
  }
175
- /** 2c session-sync IMPORT side — the inverse of `exportManifest`/`getBlob`: persist a snapshot's content
176
- * (manifest + content-addressed blobs) that was EXPORTED from ANOTHER backend (the local→cloud transfer: a src
177
- * snapshot's manifest+blobs land here as a NEW `(scope,key)`). Unlike `snapshot()` this does NOT walk an env — the
178
- * manifest is given; bytes come from `srcGetBlob(blobHash)`. Mirrors core's InMemory/File `importManifest` fail-closed
179
- * matrix verbatim (core 1.141.0). Reuses snapshot()'s EXACT putBlob (created_at-refreshing upsert, GC-grace-safe)
180
- * + manifest-tx (re-check under txn) patterns.
181
- *
182
- * NEVER-throws + FAIL-CLOSED (the matrix core's dual-review fixed, point-named to the durable twins):
183
- * - CREATE-ONCE: an entryId's tree is immutable → an existing OR in-flight `(scope,key)` is a no-op `{ok:true}`.
184
- * - `srcGetBlob` is a CROSS-BACKEND fetch (remote/IPC) that CAN REJECT (timeout/ECONNRESET/query error) — caught and
185
- * encoded as `read_failed`, NEVER thrown out of the seam.
186
- * - a MISSING blob (`bytes===undefined`) → `read_failed`.
187
- * - content-address INTEGRITY: `sha256(bytes) !== hash` → `read_failed` (a 2c transfer can cross net/IPC; a corrupt
188
- * blob stored under a mismatched hash would let a later restore silently write WRONG bytes — verify, don't trust).
189
- * - ANY blob failure → EARLY-RETURN `{ok:false}` and the manifest tx is NEVER run, so `has()` stays false (no
190
- * half-snapshot). The DISTINCT blobs already staged before the failing one are orphans (no manifest references
191
- * them) — their snapshot_blob INDEX rows are reaped (and, under MinIO, the objects deleted) by the async grace-
192
- * window sweepOrphanBlobs reaper; they never make this `(scope,key)` visible. */
193
78
  async importManifest(scope, key, manifest, srcGetBlob) {
194
- // flightKey 分隔符统一 \x00(与 snapshot 一致;此前 pg 双胞用空格=两方法 in-flight 互斥键空间分裂,
195
- // 并发 snapshot/import 同一 (scope,key) 不互斥——create-once tx 兜底故无损坏,但键空间应当同一)。
196
79
  const flightKey = `${scope}\x00${key}`;
197
80
  if (this.inFlight.has(flightKey))
198
- return { ok: true }; // create-once (mirrors snapshot)
81
+ return { ok: true };
199
82
  this.inFlight.add(flightKey);
200
83
  try {
201
- // M4(接入审计):同 snapshot——create-once 前置 has() 探针在 try 内,DB 瞬态错不逃逸 never-throw seam。
202
84
  if (await this.has(scope, key))
203
85
  return { ok: true };
204
- // STAGE every DISTINCT blob first (content-addressed: fetch each hash once). Any failure here EARLY-RETURNS before
205
- // the manifest tx runs → fail-closed: no manifest row ⇒ has() false ⇒ never a half-snapshot a restore would
206
- // silently truncate. Upsert REFRESHES created_at (incl dedup hits) → blobs touched=now, GC-grace-protected.
207
86
  for (const hash of new Set(manifest.values())) {
208
- // D2(试剂盒揪出,RB-361 同源):路径逃逸型 hash 在任何 fetch **之前**拒,文案逐字同 core
209
- // File/InMemory 后端(消费方按文案分支不劈叉);SQL 侧 hash 虽只是字符串键,但 srcGetBlob
210
- // 是跨后端回调(2c 传输面可能远端),可疑值不外递。
211
87
  if (hash === "" || hash === "." || hash === ".." || !/^[A-Za-z0-9_.-]+$/.test(hash)) {
212
88
  return { ok: false, error: { code: "read_failed", message: `unsafe blob hash ${JSON.stringify(hash)}` } };
213
89
  }
@@ -216,23 +92,19 @@ export class SqlFileSnapshotStore {
216
92
  bytes = await srcGetBlob(hash);
217
93
  }
218
94
  catch (err) {
219
- // NEVER throw out of the seam — a rejecting cross-backend fetch is `read_failed` (parity with InMemory/File).
220
95
  return { ok: false, error: { code: "read_failed", message: `source blob ${hash} fetch failed: ${err.message}` } };
221
96
  }
222
97
  if (!bytes)
223
98
  return { ok: false, error: { code: "read_failed", message: `missing source blob ${hash}` } };
224
- // Content-address integrity — verify ONCE here (the durable backend doesn't blindly trust the transferred bytes).
225
99
  if (createHash("sha256").update(bytes).digest("hex") !== hash) {
226
100
  return { ok: false, error: { code: "read_failed", message: `source blob ${hash} fails content-address integrity` } };
227
101
  }
228
- await this.putBlobRow(hash, bytes); // the SAME created_at-refreshing upsert snapshot()/putBlob use (no SQL drift)
102
+ await this.putBlobRow(hash, bytes);
229
103
  }
230
- // Commit the manifest atomically ONLY after every distinct blob is staged+verified (re-check has() under the txn so
231
- // a concurrent winner isn't duplicated) — the SAME tx pattern as snapshot().
232
104
  await this.tx(async (conn) => {
233
105
  const exists = await conn.query(this.q("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", "SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1"), [scope, key]);
234
106
  if (exists.rows[0])
235
- return; // a concurrent import/snapshot won — create-once
107
+ return;
236
108
  for (const [relPath, hash] of manifest) {
237
109
  await conn.query(this.q("INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES (?,?,?,?)", "INSERT INTO snapshot_manifest (scope, snap_key, rel_path, blob_hash) VALUES ($1,$2,$3,$4)"), [scope, key, relPath, hash]);
238
110
  }
@@ -240,19 +112,13 @@ export class SqlFileSnapshotStore {
240
112
  return { ok: true };
241
113
  }
242
114
  catch (err) {
243
- // A persist (manifest tx) fs/db error must NOT throw out of the never-throw seam either — encode it. `restore_failed`
244
- // mirrors core's File backend's outer catch (the manifest-PERSIST failure code, distinct from the fetch-side read_failed).
245
115
  return { ok: false, error: { code: "restore_failed", message: `import persist failed: ${err.message}` } };
246
116
  }
247
117
  finally {
248
118
  this.inFlight.delete(flightKey);
249
119
  }
250
120
  }
251
- /** Converge `root` back to the snapshot (applyManifest: PHASE-1 remove symlinks/strays, PHASE-2 write captured). */
252
121
  async restore(scope, key, env, root, signal) {
253
- // never-throw seam 全覆盖(L5,接入审计):入口 SELECT 也在 try 内——DB 层异常编码为 restore_failed。
254
- // applyManifest calls getBlob UN-try/caught; a TRANSIENT MinIO fault THROWS from getBlob (so a restore fails LOUD,
255
- // not silently-as-missing — finding 11). The same wrap turns that throw into {ok:false, restore_failed}.
256
122
  try {
257
123
  const { rows } = await this.db.query(this.q("SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = ? AND snap_key = ?", "SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2"), [scope, key]);
258
124
  if (rows.length === 0)
@@ -268,40 +134,25 @@ export class SqlFileSnapshotStore {
268
134
  const { rows } = await this.db.query(this.q("SELECT 1 FROM snapshot_manifest WHERE scope = ? AND snap_key = ? LIMIT 1", "SELECT 1 FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2 LIMIT 1"), [scope, key]);
269
135
  return Boolean(rows[0]);
270
136
  }
271
- /** 2c session-sync: enumerate every snapshot key (entryId) in a scope — the SAME enumeration reap()
272
- * does (`SELECT DISTINCT snap_key`), exposed so 2c can export a session's full set of file snapshots. */
273
137
  async listKeys(scope) {
274
138
  const { rows } = await this.db.query(this.q("SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = ?", "SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = $1"), [scope]);
275
139
  return rows.map((r) => String(r.snap_key));
276
140
  }
277
- /** 2c session-sync: the manifest (relPath → blobHash) for one snapshot — to EXPORT its content to another
278
- * backend byte-for-byte (paired with getBlob for content-addressed transfer). Reads the STORED manifest rows (never
279
- * re-hashes). 0 rows = no snapshot (a real snapshot always has ≥1 manifest row) → null (parity with InMemory). */
280
141
  async exportManifest(scope, key) {
281
142
  const { rows } = await this.db.query(this.q("SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = ? AND snap_key = ?", "SELECT rel_path, blob_hash FROM snapshot_manifest WHERE scope = $1 AND snap_key = $2"), [scope, key]);
282
143
  if (rows.length === 0)
283
- return null; // absent (an empty-tree snapshot still has ≥1 row; 0 rows ⇒ no such snapshot)
144
+ return null;
284
145
  return new Map(rows.map((r) => [String(r.rel_path), String(r.blob_hash)]));
285
146
  }
286
- /** 2c session-sync: the content-addressed bytes for `hash` (undefined if absent) — reads via the BlobBackend
287
- * (SQL default or MinIO, where getBlob also verifies content-address integrity). */
288
147
  async getBlob(hash) {
289
148
  return this.blobs.getBlob(hash);
290
149
  }
291
- /** The subset of `hashes` already present in the blob store — reads the SQL INDEX (one IN(...) for BOTH backends),
292
- * so the §10 import blob-presence pre-check is a cheap index probe, NOT N full-object getBlob GETs (finding 12). */
293
150
  async hasBlobs(hashes) {
294
151
  return this.blobs.hasBlobs(hashes);
295
152
  }
296
- /** workspace 浏览面(#3,[1894]②):per-blob 字节数——读 SQL INDEX 的 byte_len(单 IN 查询;
297
- * MinIO 后端的索引行同带 byte_len,两后端同源)。tree 的 size 投影 + file 面「超限先拒后拉」用。 */
298
153
  async blobSizes(hashes) {
299
154
  return sqlBlobSizes(this.db.dialect, this.pool, hashes);
300
155
  }
301
- /** GC: drop every snapshot in `scope` whose key is NOT in keepKeys. Drops ONLY the SQL manifest rows synchronously —
302
- * the now-unreferenced blobs become ORPHANS (their snapshot_blob INDEX rows survive) and are collected LATER by the
303
- * async sweepOrphanBlobs reaper (NEVER a synchronous external-store delete that could block/fail this reap on a MinIO
304
- * blip — clay 2026-06-26 async-purge design). */
305
156
  async reap(scope, keepKeys) {
306
157
  return this.tx(async (conn) => {
307
158
  const { rows: keys } = await conn.query(this.q("SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = ?", "SELECT DISTINCT snap_key FROM snapshot_manifest WHERE scope = $1"), [scope]);
@@ -312,24 +163,10 @@ export class SqlFileSnapshotStore {
312
163
  return drop.length;
313
164
  });
314
165
  }
315
- /** E21 purge — drop ALL snapshots for a session (every key). Drops ONLY the SQL manifest rows synchronously → the
316
- * session is immediately gone + its blobs become orphans; the actual MinIO/SQL BYTE deletion is the async
317
- * sweepOrphanBlobs reaper's job (bounded-window EVENTUAL CONSISTENCY satisfies the E21 privacy/compliance "really
318
- * delete the bytes" requirement WITHOUT a fragile synchronous object-store delete on the purge path). Scoped by sessionId. */
319
166
  async deleteBySession(scope) {
320
167
  const { affected } = await this.db.query(this.q("DELETE FROM snapshot_manifest WHERE scope = ?", "DELETE FROM snapshot_manifest WHERE scope = $1"), [scope]);
321
168
  return affected;
322
169
  }
323
- /** Content-addressed blobs are shared across snapshots → GC only those NO manifest references. The orphan-hash SET is
324
- * computed from the `snapshot_blob` INDEX in SQL (reference tracking stays in the DB — works for BOTH the SQL backend
325
- * AND MinIO, whose index rows are kept bytes-NULL); the BlobBackend's deleteBlobs applies the grace window (a just-
326
- * written blob a concurrent in-flight snapshot needs is never delete-in-use) and, for MinIO, deletes the objects.
327
- * Returns the count ACTUALLY deleted (the grace-passed subset deleteBlobs reports — NOT the pre-grace orphan count,
328
- * finding 13). Called ONLY from the async sweepOrphanBlobs reaper (never the synchronous purge path).
329
- *
330
- * The orphan-SET query text is IDENTICAL for both dialects (no placeholders) — a single shared statement, not a `q()`
331
- * pair. A query error degrades to "zero orphans this sweep" (both twins already treated it that way — the reaper
332
- * retries next tick, so under-collecting once is harmless; never surfaced as a hard failure). */
333
170
  async gcOrphanBlobs() {
334
171
  const { rows } = await this.db
335
172
  .query("SELECT blob_hash FROM snapshot_blob WHERE blob_hash NOT IN (SELECT blob_hash FROM snapshot_manifest)")
@@ -337,16 +174,11 @@ export class SqlFileSnapshotStore {
337
174
  const orphans = rows.map((r) => String(r.blob_hash));
338
175
  if (orphans.length === 0)
339
176
  return 0;
340
- return this.blobs.deleteBlobs(orphans); // grace-passed subset actually deleted (bounded concurrency inside)
177
+ return this.blobs.deleteBlobs(orphans);
341
178
  }
342
- /** 2c session-sync DoS bound + the ASYNC byte-GC for reap/deleteBySession orphans — GLOBALLY GC grace-passed
343
- * orphan blobs (standalone PUTs never imported AND the blobs a purge/reap left orphaned). The driver is the main.ts
344
- * reaper (scheduled, error-tolerant: a per-object MinIO delete failure is skipped + retried next sweep → bounded-
345
- * window eventual consistency). Reuses {@link gcOrphanBlobs} VERBATIM; returns the count actually deleted this sweep. */
346
179
  async sweepOrphanBlobs() {
347
180
  return this.gcOrphanBlobs();
348
181
  }
349
- /** Run statements atomically; rolls back on failure. */
350
182
  async tx(fn) {
351
183
  const conn = await this.db.connect();
352
184
  try {
@@ -360,7 +192,6 @@ export class SqlFileSnapshotStore {
360
192
  await conn.rollback();
361
193
  }
362
194
  catch {
363
- /* ignore */
364
195
  }
365
196
  throw err;
366
197
  }
@@ -369,13 +200,11 @@ export class SqlFileSnapshotStore {
369
200
  }
370
201
  }
371
202
  }
372
- /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
373
203
  export class TiDBFileSnapshotStore extends SqlFileSnapshotStore {
374
204
  constructor(pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
375
205
  super(mysqlDriver(pool), pool, blobBackend, bounds);
376
206
  }
377
207
  }
378
- /** PostgreSQL binding — historical class name + ctor shape preserved. */
379
208
  export class PgFileSnapshotStore extends SqlFileSnapshotStore {
380
209
  constructor(pool, blobBackend, bounds = DEFAULT_SNAPSHOT_BOUNDS) {
381
210
  super(pgDriver(pool), pool, blobBackend, bounds);
@@ -1,28 +1,3 @@
1
- /**
2
- * ForkRoutingSessionStore — the subRunner's session backing after core 1.350's fork 终修([1364]①/[1367]①).
3
- *
4
- * 问题形:core 的 `hostSessionFork` 在 HOST runner 的店里 fork,fork 子任务带 `requireExistingSession:true`
5
- * 在 opts.runner(subRunner)上执行——subRunner 拆店时子任务查不到刚 fork 出的会话,响亮失败
6
- * `resume.session_not_found`(cli 真机复现)。
7
- *
8
- * 但把 host durable 店**整个**给 subRunner(第一版修)被 codex 判两 high:普通子任务(council 镜头/
9
- * team/子代理)会把每次运行都铸成 committed durable 会话——sweepStagingSessions 只扫**无 session_meta**
10
- * 的 staging 弃置行,committed 子会话没有 reaper=无界增长;且进 owner 过滤的会话枚举(零 run 的内部
11
- * 会话污染 /v1/sessions)。
12
- *
13
- * 此包装店按**会话的真居所**路由,两边语义都保住:
14
- * - `acquire(id, {requireExisting})`(fork 子 + retained-child resume 的形)→ **host 店优先**,miss 落
15
- * transient(retained 瞬态子的 resume 仍走私店);两边都 miss=抛 host 侧错误(fail-loud 原语义)。
16
- * - `acquire()`(无 id=普通子任务铸新会话)→ **transient**(throwaway 姿势保留:council/team 子会话
17
- * 不落 durable、不进枚举、TTL 自清)。
18
- * - `acquire(id)`(带 id 不 requireExisting)→ 已有路由按路由走;未知 id 落 transient(claim-create
19
- * 绝不打到 durable 店——那是 authorizer 的职权)。
20
- * - `fork(sourceId)` → 源在哪店在哪店 fork(路由未知时 host 先试,null 再 transient);新 id 记路由。
21
- * - `list`/`size`/`dispose` = transient 侧(host 店归 main runner 所有;不双列、不双清)。
22
- *
23
- * 路由表 LRU 上限 {@link ROUTE_MAX}:满额逐最老(refresh-on-use);被逐后下次 `acquire` 按上述规则重derive
24
- * (requireExisting 的 host-first 重试仍落对店),中途 touch/noteTaskRun 缺路由=落 transient(no-op 级)。
25
- */
26
1
  import { SessionError } from "@sema-agent/core";
27
2
  import { recordFailOpen } from "../observability/fail-open.js";
28
3
  const ROUTE_MAX = 8192;
@@ -30,36 +5,20 @@ export class ForkRoutingSessionStore {
30
5
  host;
31
6
  transient;
32
7
  route = new Map();
33
- /** pin 时的店归属——unpin 恒回同店(LRU 逐出不拆对,见 pin/unpin)。 */
34
8
  pinnedAt = new Map();
35
- /** [1593] 晋升名册(探针 Finding A 修):已 promoteToHost 的 id **永久**钉 host——路由表会被 LRU
36
- * 逐出,而私店的 forget 只清 entries 缓存行、底层 repo 行不清且 sweep 扫不到(core session-store.js
37
- * 亲读),逐出后 transient-first 的 requireExisting 会把陈旧快照端回给 revive。本集不逐出(上界=
38
- * parked 子代数,天然小);pick/acquire 首查。 */
39
9
  promoted = new Set();
40
- /** #292 placement 半场:placed id 名册(与 promoted 同姿势——不随路由 LRU 逐出)。placed 会话的
41
- * 真居所恒=host(分区在 durable 店),任何形的 acquire/touch/release 都不得落 transient——
42
- * 否则 transient 的 claim-create 会铸出同 id 影子会话,requireExisting 的 transient-first 顺序
43
- * 会让影子遮蔽真转录(F-7 顺序对 placed 反向成害)。 */
44
10
  placed = new Set();
45
- /** #292:host 店的 placements 声明透传(声明制——包装层不 duck-type,host 声明了才武装 placed
46
- * 路由;host 不声明时本店全行为与 5.47 前逐字节同)。 */
47
11
  placements;
48
- /** #292:分区枚举透传(host 有才有——恒在的空实现会把「不能枚举」谎报成「没有 placed 会话」)。 */
49
12
  listPlaced;
50
13
  constructor(host, transient) {
51
14
  this.host = host;
52
15
  this.transient = transient;
53
- // #292 声明/枚举面:host 声明什么透传什么(transient 永不声明 placements——分区住 durable 店)。
54
16
  if (host.placements !== undefined)
55
17
  this.placements = host.placements;
56
18
  const hostListPlaced = host.listPlaced;
57
19
  if (typeof hostListPlaced === "function") {
58
20
  this.listPlaced = (kind, o) => hostListPlaced.call(host, kind, o);
59
21
  }
60
- // backlog(hasSessionFork 恒真):两 backing 都无 fork 面时,包装店的 fork 方法**摘除**——core 的
61
- // fork 门(hasSessionFork(opts.runner.sessions))才能诚实报 fork.unavailable,而不是放行后折叠成
62
- // 误导性的「source not found」。现三 backing 均有 fork,此臂为接入无 fork durable backend 备。
63
22
  if (host.fork === undefined && transient.fork === undefined) {
64
23
  this.fork = undefined;
65
24
  }
@@ -75,16 +34,12 @@ export class ForkRoutingSessionStore {
75
34
  }
76
35
  pick(id) {
77
36
  if (this.promoted.has(id) || this.placed.has(id))
78
- return this.host; // 名册优先(路由可被 LRU 逐出,名册不逐;placed 同姿势 #292)
37
+ return this.host;
79
38
  const r = this.route.get(id);
80
39
  if (r !== undefined)
81
- this.remember(id, r); // refresh recency
40
+ this.remember(id, r);
82
41
  return r === "host" ? this.host : this.transient;
83
42
  }
84
- /** #292:host 声明分区时的冷缓存判位探针。判据=host `requireExisting` acquire 的 `SessionMetadata.placement`
85
- * (5.48 additive,File/SQL/Ttl 三形都带——比 `placementOf` 面更普适:Ttl 缓存层不透传 placementOf)。
86
- * placed 命中把 acquired 会话一并交回(调用方正需要它时零二次 acquire);非 placed/absent 的探针
87
- * acquire 一律 **forget** 收尾([4743] 义务①:验存在的 acquire 禁 release——release(placed)=真删)。 */
88
43
  async hostPlacedProbe(id) {
89
44
  if (this.placed.has(id)) {
90
45
  const acquired = await this.host.acquire(id, { requireExisting: true });
@@ -97,9 +52,6 @@ export class ForkRoutingSessionStore {
97
52
  acquired = await this.host.acquire(id, { requireExisting: true });
98
53
  }
99
54
  catch (err) {
100
- // not_found=常态(绝大多数 id 非 placed),静默折 absent;**真故障**(host 不可达/读错)同样折
101
- // absent 但计数留痕(#191 门②:F 类兜底必走 recordFailOpen)——放行的是 transient 腿在 host
102
- // 故障窗内保持独立可用,代价与恢复路径见 tag 注册表。
103
55
  if (!(err instanceof SessionError && err.code === "not_found")) {
104
56
  recordFailOpen("server.fork-routing.placed-probe-host-error", err instanceof Error ? err.message : String(err));
105
57
  }
@@ -110,7 +62,7 @@ export class ForkRoutingSessionStore {
110
62
  this.placed.add(id);
111
63
  return { state: "placed", acquired };
112
64
  }
113
- await this.host.forget?.(id); // 只为验居所:丢缓存引用,内容不动(禁 release)
65
+ await this.host.forget?.(id);
114
66
  return { state: "unplaced" };
115
67
  }
116
68
  async acquire(sessionId, opts) {
@@ -119,9 +71,6 @@ export class ForkRoutingSessionStore {
119
71
  this.remember(a.sessionId, "transient");
120
72
  return a;
121
73
  }
122
- // #292 建会话入分区形(受信 spawner:带 placement、不 requireExisting)——分区住 host durable 店;
123
- // host 不声明 placements 时按旧行为落 transient(core 铸造门本就不会给这种部署铸 placement,
124
- // 这臂只是防御性一致)。
125
74
  if (opts?.placement !== undefined && opts.requireExisting !== true && this.placements?.subagent !== undefined) {
126
75
  const a = await this.host.acquire(sessionId, opts);
127
76
  this.placed.add(sessionId);
@@ -129,8 +78,6 @@ export class ForkRoutingSessionStore {
129
78
  return a;
130
79
  }
131
80
  if (this.promoted.has(sessionId) || this.placed.has(sessionId)) {
132
- // [1593] 晋升名册:恒走 host——绝不能让 transient-first 把 forget 后仍活在私店 repo 里的陈旧
133
- // 快照端给 revive(探针 Finding A)。placed 名册同规则(#292:影子遮蔽真转录的反向 F-7)。
134
81
  const a = await this.host.acquire(sessionId, opts);
135
82
  this.remember(sessionId, "host");
136
83
  return a;
@@ -142,19 +89,13 @@ export class ForkRoutingSessionStore {
142
89
  return a;
143
90
  }
144
91
  if (opts?.requireExisting) {
145
- // PAIR-REVIEW F-7(codex 过审):**transient-first**——retained 瞬态子 resume 直达私店;fork 子恒不在
146
- // transient(miss)→ fallback host 命中,语义不变。顺序反转封住 F-7 组合链:同 id 在两店并存时
147
- // (child uuid 被同租户误提交到主 API 而 host 店 claim-create 出空壳),真子会话(transient)赢,
148
- // 空壳不遮蔽。双 miss 抛 host 侧错误(响亮,fail-closed 到「真不存在」)。
149
- // #292 例外:transient 命中且 host 声明分区时,还要过一次 placed 判位——同 id 若在 host 是 placed
150
- // 会话,transient 那份是影子(claim-create 铸的),真转录必须赢(F-7 顺序对 placed 反向成害)。
151
92
  let transientErr;
152
93
  try {
153
94
  const a = await this.transient.acquire(sessionId, opts);
154
95
  if (this.placements?.subagent !== undefined) {
155
96
  const probe = await this.hostPlacedProbe(sessionId);
156
97
  if (probe.state === "placed" && probe.acquired !== undefined) {
157
- await this.transient.forget?.(sessionId); // 影子只丢引用(私店 repo 行本就 sweep 不到,名册是真防线)
98
+ await this.transient.forget?.(sessionId);
158
99
  this.remember(sessionId, "host");
159
100
  return probe.acquired;
160
101
  }
@@ -167,8 +108,6 @@ export class ForkRoutingSessionStore {
167
108
  }
168
109
  try {
169
110
  const a = await this.host.acquire(sessionId, opts);
170
- // 顺手:host 端回的会话若带 placement 元组,入名册(后续零探针直达)。只在声明分区的部署上读
171
- // (非声明形零行为差),且 meta 读失败不阻断 acquire(名册只是省探针的缓存,不是判定)。
172
111
  if (this.placements?.subagent !== undefined) {
173
112
  try {
174
113
  const meta = await a.session.getMetadata();
@@ -176,8 +115,6 @@ export class ForkRoutingSessionStore {
176
115
  this.placed.add(sessionId);
177
116
  }
178
117
  catch (err) {
179
- // 名册只是省探针的缓存,不是判定——缺一条=该 id 下次多付一次 hostPlacedProbe,正确性零影响;
180
- // 计数留痕(#191 门②)让「meta 一直读失败」的环境病在遥测显形。
181
118
  recordFailOpen("server.fork-routing.placed-roster-meta-unreadable", err instanceof Error ? err.message : String(err));
182
119
  }
183
120
  }
@@ -189,16 +126,13 @@ export class ForkRoutingSessionStore {
189
126
  throw hostErr;
190
127
  }
191
128
  }
192
- // 带 id 但不要求已存在:claim-create 只允许落 transient(durable 店的 claim-create 是 authorizer 的职权)。
193
- // #292 claim closure(义务⑥过墙形):id 若已是 host 分区的 placed 会话,claim-create 落 transient 会
194
- // 铸同 id 影子——委托给 host claim 形,让其 placement_refused 响亮拒(绝不静默铸影子)。
195
129
  if (this.placements?.subagent !== undefined) {
196
130
  const probe = await this.hostPlacedProbe(sessionId);
197
131
  if (probe.state === "placed") {
198
- await this.host.forget?.(sessionId); // 探针引用丢弃(refusal 路径不把会话交出)
199
- const a = await this.host.acquire(sessionId, opts); // claim 形打声明店 ⇒ placement_refused(义务⑥)
132
+ await this.host.forget?.(sessionId);
133
+ const a = await this.host.acquire(sessionId, opts);
200
134
  this.remember(sessionId, "host");
201
- return a; // 不可达(上一行必抛);保留形以防 host 语义演化时静默吞
135
+ return a;
202
136
  }
203
137
  }
204
138
  const a = await this.transient.acquire(sessionId, opts);
@@ -212,16 +146,11 @@ export class ForkRoutingSessionStore {
212
146
  return this.pick(sessionId).release(sessionId);
213
147
  }
214
148
  forget(sessionId) {
215
- // PAIR-REVIEW F-8:forget=显式移除 → 路由项同步清(死 id 不再占 LRU 槽挤活路由);release 保留
216
- // 路由(retained 子 resume 会再 acquire,清了只是多付一次 derive)。
217
149
  const target = this.pick(sessionId);
218
150
  this.route.delete(sessionId);
219
151
  return target.forget?.(sessionId);
220
152
  }
221
153
  pin(sessionId) {
222
- // backlog 潜伏钉(pin/unpin 路由不对称):pin 时 refresh 路由近度(remember 已在 pick 内做)+把
223
- // pinned id 记入独立集合——unpin 优先按 pin 时的店走,LRU 逐出不再能把 pin/unpin 拆到两家店
224
- // (host 店永久 pinned 不可回收的潜伏形就此封死)。
225
154
  const r = this.route.get(sessionId);
226
155
  if (r !== undefined)
227
156
  this.pinnedAt.set(sessionId, r);
@@ -241,23 +170,6 @@ export class ForkRoutingSessionStore {
241
170
  list() {
242
171
  return this.transient.list?.() ?? Promise.resolve([]);
243
172
  }
244
- /**
245
- * [1593] 子代 park 晋升:把住在 transient(TTL)店的子会话**整树拷贝**进 host durable 店,然后
246
- * 让该 id 从此路由到 host——`ensureChildSessionDurable` 的「主动晋升」半场(1.263 的只查不迁形在
247
- * 拆店生产形下恒否决 park:子会话从不在 host 店,core 收 veto 即 expire checkpoint + settle failed,
248
- * cli 真机 14ms~2s expired 即此)。
249
- *
250
- * 三步缺一不可:
251
- * 1. 拷贝:transient 读整树(getEntries)+ 源 leaf → host 店 acquire(id)(durable 店的 acquire
252
- * 缺行即建行)→ storage.appendEntry 逐条 verbatim(重复 id 容忍=幂等续迁:上次晋升半途崩的
253
- * 残留行跳过)→ setLeafId(源 leaf)(分支树以源为准,不信 append 推进的 leaf)。
254
- * 2. transient.forget(id):`requireExisting` 的 acquire 是 **transient-first**(F-7 裁定)——私店
255
- * 副本不移除,revive 会续写进 1h TTL 副本,durable park 名存实亡。
256
- * 3. 路由记 host(此后 touch/pin/release 全走 host;pinnedAt 若记着 transient 一并改写,
257
- * unpin 不落错店)。
258
- *
259
- * 已在 host(路由或真行)⇒ 幂等直返。两店都无此会话 ⇒ 抛(调用方=park veto,诚实 fail-closed)。
260
- */
261
173
  async promoteToHost(sessionId) {
262
174
  if (this.promoted.has(sessionId))
263
175
  return;
@@ -270,26 +182,17 @@ export class ForkRoutingSessionStore {
270
182
  src = await this.transient.acquire(sessionId, { requireExisting: true });
271
183
  }
272
184
  catch {
273
- // 私店无此会话:可能已在 host(直查确认=幂等),否则如实抛(host 侧错误响亮上浮)
274
185
  const probe = await this.host.acquire(sessionId, { requireExisting: true });
275
- // #292 preflight 纪律([4743] 义务①):**只为验存在**的 acquire 收尾一律 forget —— host 店的
276
- // release 对 placed 会话是真删除(core 5.48 义务 4),用它给一次探活收尾就是拿删除当探针。
277
186
  await this.host.forget?.(probe.sessionId);
278
187
  this.promoted.add(sessionId);
279
188
  this.remember(sessionId, "host");
280
189
  return;
281
190
  }
282
- // ⚠️ 私店源在此**不 release**:TtlSessionStore.release 对非 pinned 会话=整删(F3 教训)——拷贝
283
- // 失败时删源=毁掉仍在跑/仍可赎回的子会话。成功路径由下方 forget(纯 entries 移除,无 repo 删)收尾;
284
- // 失败路径源原样留活,veto 如实上浮。
285
191
  const entries = await src.session.getEntries();
286
192
  const leaf = await src.session.getLeafId();
287
193
  const dst = await this.host.acquire(sessionId);
288
194
  try {
289
195
  const storage = dst.session.getStorage();
290
- // 探针 Finding B 修:core 各 storage 的 appendEntry 对重复 id **不抛不查**(storage-base 亲读)
291
- // ——靠 try/catch 实现幂等是空想,断续晋升会把同 id 双写成脏树(且脏树会被 import-validate 的
292
- // 重复 id 门拒)。真幂等 = 先读 dst 现存 id 集,set-diff 后只补缺。
293
196
  const existing = new Set((await dst.session.getEntries()).map((e) => e.id));
294
197
  for (const entry of entries) {
295
198
  if (existing.has(entry.id))
@@ -302,18 +205,12 @@ export class ForkRoutingSessionStore {
302
205
  throw new Error(`session "${sessionId}" promotion verification failed (leaf mismatch after import)`);
303
206
  }
304
207
  finally {
305
- // #292:晋升写完只需**丢引用**(内容必须留下,那正是晋升的目的)⇒ forget,不是 release
306
- // (同上:release 对 placed 会话已是真删除,而这条 finally 连失败路径都会走)。
307
208
  await this.host.forget?.(sessionId);
308
209
  }
309
210
  try {
310
211
  await this.transient.forget?.(sessionId);
311
212
  }
312
213
  catch {
313
- // forget 失败不阻断晋升。⚠️ forget 本就只清私店的 entries 缓存行——底层 repo 行不清、sweep 也
314
- // 扫不到(探针 Finding A 取证),陈旧快照在私店**永生**;对 revive 的真防线是上面的晋升名册
315
- // (promoted 集恒指 host,不随路由 LRU 逐出),forget 只是缩内存的 best-effort。transient.pinned
316
- // 可能残留该 id(forget 不清 pin 集)——unpin 已随 pinnedAt 改写落 host,残留仅是空集合项。
317
214
  }
318
215
  if (this.pinnedAt.get(sessionId) === "transient")
319
216
  this.pinnedAt.set(sessionId, "host");
@@ -340,7 +237,7 @@ export class ForkRoutingSessionStore {
340
237
  return this.transient.size;
341
238
  }
342
239
  dispose() {
343
- return this.transient.dispose(); // host 店归 main runner 所有,绝不在此清
240
+ return this.transient.dispose();
344
241
  }
345
242
  }
346
243
  //# sourceMappingURL=fork-routing-session-store.js.map