@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,7 @@
1
- /**
2
- * BlobBackend — the content-addressed BYTE store for the durable FileSnapshotStore (E19 / 2c), DECOUPLED from the
3
- * manifest. clay 2026-06-26: the conversation TRANSCRIPT stays DB-first; the ONLY thing that moves to an object store
4
- * is the file-snapshot BLOBS (binary file content — a perf-suboptimal TiDB blob row; the tree bound is core's
5
- * DEFAULT_SNAPSHOT_BOUNDS 256 MiB with NO per-file cap, hence the (a) SQL per-blob cap below). This seam
6
- * lets the tidb/pg file-snapshot stores keep their MANIFEST in SQL (`snapshot_manifest`, the hot relPath→hash rows the
7
- * transcript read NEVER touches) while routing the heavy bytes either to SQL (`snapshot_blob.bytes`, the DEFAULT — a
8
- * MINIO-unset deployment is byte-identical to today) or to MinIO (the offload).
9
- *
10
- * THE SQL BLOB INDEX (clay 2026-06-26 design correction). EVEN under the MinIO backend, every blob keeps an INDEX row in
11
- * `snapshot_blob` — `(blob_hash, byte_len, created_at)` with `bytes` NULL (the bytes live in MinIO). This is the load-
12
- * bearing decision: ALL the reference-tracking (gcOrphanBlobs computes the orphan SET = `snapshot_blob` rows NOT IN
13
- * `snapshot_manifest`) and the created_at GRACE WINDOW (delete-in-use protection) read that index, so they work
14
- * IDENTICALLY for both backends. WITHOUT the index, a MinIO deployment would never GC its objects AND an E21 session
15
- * purge would leave the file CONTENT in MinIO forever (a privacy/compliance breach). With it, a purged session's blobs
16
- * become orphans (no manifest) and are EVENTUALLY collected by the async sweepOrphanBlobs reaper.
17
- *
18
- * ASYNC byte deletion (clay 2026-06-26): MinIO objects are NEVER deleted on the synchronous E21 purge / reap path (a
19
- * synchronous external-store delete could block/fail the whole purge on a network blip). The purge deletes ONLY the SQL
20
- * manifest rows → the blob becomes an orphan; the periodic `sweepOrphanBlobs` reaper (main.ts) deletes the MinIO objects
21
- * + the index rows LATER, error-tolerantly (a single MinIO delete failure is skipped + retried next sweep) → bounded-
22
- * window EVENTUAL CONSISTENCY. {@link BlobBackend.deleteBlobs} is therefore only ever called from the reaper.
23
- *
24
- * REUSE iron rule (core-service-boundary / no-stopgap): {@link SqlBlobBackend} EXTRACTS the `snapshot_blob` ops from the
25
- * tidb/pg stores VERBATIM (the created_at-refreshing upsert, the SELECT, the grace-window orphan DELETE), so the SQL
26
- * default path is byte-identical to before this refactor. {@link MinioBlobBackend} REUSES the existing zero-dependency
27
- * {@link presignS3Url} SigV4 generator + `fetch` (the repo's no-SDK posture — OTLP + the k8s adapter are zero-dep too);
28
- * it NEVER pulls in an S3 SDK, and it REUSES the SqlBlobBackend's index ops for the (blob_hash, byte_len, created_at)
29
- * rows (the bytes-NULL index — no SQL drift between the two backends' reference-tracking).
30
- */
31
1
  import { createHash } from "node:crypto";
32
2
  import { presignS3Url, presignS3ListUrl } from "./s3-presign.js";
33
- /** Orphan-blob GC grace (SHARED with the file-snapshot stores' BLOB_GC_GRACE_MS) — a blob is GC-eligible only if
34
- * unreferenced AND untouched for this long; putBlob refreshes created_at on every touch (incl a dedup hit) so an
35
- * in-flight snapshot's blobs are protected from delete-in-use. 1h ≫ any bounded snapshot. */
36
3
  const BLOB_GC_GRACE_MS = 3_600_000;
37
- /** (a)(clay 拍 a+c,2026-07-27)tidb 方言的默认 per-blob 帽 = **6 MiB,TiDB 自己的默认
38
- * `txn-entry-size-limit`**。两堵墙真库实测(k3s8 tidb-test v7.5.1 默认配置,2026-07-27 探针):
39
- * 7MiB=`entry too large`(entry 墙,默认 6291456)、31MiB=同、32MiB=`max_allowed_packet`(包墙,
40
- * 64MiB ÷ ~2× 文本协议膨胀)——矮墙是 entry 的 6MiB,不是先前推测的包墙 ~31MiB。帽=诚实拒绝:
41
- * 超限 blob 在 query 之前 typed 拒(消费面 HTTP 413 / capture warn 拿到可行动信息);**帽拒绝的
42
- * 在默认配置下本来也落不进去 ⇒ 零回归**。调大过 entry 限/packet 的部署用 SNAPSHOT_BLOB_SQL_MAX_BYTES
43
- * 放宽(两方言都生效)。**pg 方言默认不设帽**(扩展协议无这两堵墙,不人为降档)。D-1 附件战役同墙
44
- * 教训:task-attachment-store.ts:19。 */
45
4
  export const SQL_BLOB_DEFAULT_MAX_BYTES = 6_291_456;
46
- /** typed 判别错:blob 超过 SQL 店的承载帽。判别码供店侧结果映射/HTTP 面/日志使用——绝不能与
47
- * transient 写失败混在一类(那类是可重试的 502;这类重试永远不会成功)。 */
48
5
  export class BlobTooLargeError extends Error {
49
6
  code = "blob_too_large_for_sql";
50
7
  constructor(byteLen, capBytes) {
@@ -55,11 +12,7 @@ export class BlobTooLargeError extends Error {
55
12
  this.name = "BlobTooLargeError";
56
13
  }
57
14
  }
58
- /** Max concurrent MinIO object ops (HEAD/DELETE) per reap/sweep batch — bound the open sockets/presigns so a sweep with
59
- * tens of thousands of orphans can't exhaust FDs / throttle MinIO (finding 22). The full hash set is processed in
60
- * windows of this size; a slow/failed object never blocks the others (each is independently error-tolerant). */
61
15
  const MINIO_OP_CONCURRENCY = 16;
62
- /** Run `fn` over `items` with at most `limit` in flight at once (a tiny bounded-concurrency pool — no dependency). */
63
16
  async function mapBounded(items, limit, fn) {
64
17
  let i = 0;
65
18
  const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
@@ -70,33 +23,15 @@ async function mapBounded(items, limit, fn) {
70
23
  });
71
24
  await Promise.all(workers);
72
25
  }
73
- /**
74
- * SQL BlobBackend — the `snapshot_blob` table ops EXTRACTED VERBATIM from the tidb/pg file-snapshot stores (the
75
- * created_at-refreshing upsert, the bytes SELECT, the grace-window orphan DELETE→deleteBlobs, a has via SELECT). Two
76
- * dialect twins selected by `dialect`. Keeping the SQL byte-identical means the existing behaviour + the env-gated
77
- * real-DB integration suite are unchanged when MinIO is unset (the DEFAULT backend the file-snapshot stores construct).
78
- *
79
- * NOTE deleteBlobs here is the BYTE delete ONLY — it does NOT do the `NOT IN (SELECT … snapshot_manifest)` orphan
80
- * filter (that reference check stays in the file-snapshot store, which computes the orphan hash SET from the manifest
81
- * and hands it here). The grace-window `created_at < cutoff` guard is RETAINED on the delete so a just-written blob
82
- * that a concurrent in-flight snapshot still needs is never byte-deleted (delete-in-use is data loss; under-delete is
83
- * harmless — the next sweep collects it).
84
- */
85
26
  export class SqlBlobBackend {
86
27
  dialect;
87
28
  pool;
88
- /** Effective per-blob cap: explicit ctor value wins; else tidb=SQL_BLOB_DEFAULT_MAX_BYTES, pg=uncapped. */
89
29
  capBytes;
90
30
  constructor(dialect, pool, maxBytes) {
91
31
  this.dialect = dialect;
92
32
  this.pool = pool;
93
33
  this.capBytes = maxBytes ?? (dialect === "tidb" ? SQL_BLOB_DEFAULT_MAX_BYTES : Number.POSITIVE_INFINITY);
94
34
  }
95
- /** The created_at-refreshing content-addressed upsert — VERBATIM the tidb/pg stores' `putBlobRow`. Refreshes
96
- * created_at even on a dedup hit so a re-used/just-written blob is "touched=now" and GC-grace-protected.
97
- * (a): an over-cap blob is REJECTED (typed {@link BlobTooLargeError}) BEFORE the driver ever sees the bytes —
98
- * the alternative is an inscrutable packet-limit error deep in the driver, and on the HTTP face a 502 that
99
- * invites the peer to retry a blob that can never land. */
100
35
  async putBlob(hash, bytes) {
101
36
  if (bytes.byteLength > this.capBytes)
102
37
  throw new BlobTooLargeError(bytes.byteLength, this.capBytes);
@@ -107,8 +42,6 @@ export class SqlBlobBackend {
107
42
  await this.pool.query("INSERT INTO snapshot_blob (blob_hash, byte_len, bytes, created_at) VALUES ($1,$2,$3,$4) ON CONFLICT (blob_hash) DO UPDATE SET created_at = EXCLUDED.created_at", [hash, bytes.byteLength, Buffer.from(bytes), new Date()]);
108
43
  }
109
44
  }
110
- /** The bytes for `hash` (undefined if absent) — VERBATIM the tidb/pg stores' getBlob SELECT. A pool/query error
111
- * PROPAGATES (a transient DB fault is loud, not a silent "missing blob" — parity with the MinIO backend's throw). */
112
45
  async getBlob(hash) {
113
46
  if (this.dialect === "tidb") {
114
47
  const [rows] = await this.pool.query("SELECT bytes FROM snapshot_blob WHERE blob_hash = ?", [hash]);
@@ -117,22 +50,14 @@ export class SqlBlobBackend {
117
50
  const b = await this.pool.query("SELECT bytes FROM snapshot_blob WHERE blob_hash = $1", [hash]);
118
51
  return b.rows[0]?.bytes != null ? new Uint8Array(b.rows[0].bytes) : undefined;
119
52
  }
120
- /** The subset of `hashes` that exist — a single `blob_hash IN (...)` SELECT (idempotency / reap probe). */
121
53
  async hasBlobs(hashes) {
122
54
  return sqlHasBlobs(this.dialect, this.pool, hashes);
123
55
  }
124
- /** Byte-delete the given hashes (the orphan SET is computed by the caller from snapshot_manifest). Retains the
125
- * grace-window guard so a just-written blob a concurrent in-flight snapshot needs is never delete-in-use — VERBATIM
126
- * the tidb/pg stores' gcOrphanBlobs delete, minus the `NOT IN (SELECT … manifest)` clause (the caller already
127
- * excluded referenced hashes). Best-effort: a delete error is swallowed (under-delete is harmless). */
128
56
  async deleteBlobs(hashes) {
129
57
  const deleted = await sqlDeleteIndexRowsPastGrace(this.dialect, this.pool, hashes);
130
58
  return deleted.length;
131
59
  }
132
60
  }
133
- /** SHARED index op — the subset of `hashes` present as a `snapshot_blob` row (the SQL INDEX). Used by SqlBlobBackend
134
- * AND MinioBlobBackend (the index is the single source of presence truth for both — cheaper + consistent than a
135
- * HEAD-per-hash on MinIO; the bytes-NULL MinIO index row still counts as present). */
136
61
  async function sqlHasBlobs(dialect, pool, hashes) {
137
62
  if (hashes.length === 0)
138
63
  return new Set();
@@ -146,16 +71,10 @@ async function sqlHasBlobs(dialect, pool, hashes) {
146
71
  const r = await pool.query(`SELECT blob_hash FROM snapshot_blob WHERE blob_hash IN (${ph})`, distinct);
147
72
  return new Set(r.rows.map((row) => row.blob_hash));
148
73
  }
149
- /** SHARED index op(workspace 浏览面 #3,[1894]②)—— per-blob 字节数,读 SQL INDEX 的 `byte_len`
150
- * 列(sqlHasBlobs 同款单 IN 查询;MinIO 后端的 bytes-NULL 索引行同样带 byte_len,两后端同源)。
151
- * tree/file 面的 size 投影与「超限先拒后拉」都靠它——绝不为拿大小去 getBlob 整块字节。 */
152
74
  export async function sqlBlobSizes(dialect, pool, hashes) {
153
75
  if (hashes.length === 0)
154
76
  return new Map();
155
77
  const distinct = [...new Set(hashes)];
156
- // 🔴 复审 F7:分块。workspace list 腿对每个快照各查一次全 manifest 的 unique hash——按 core 上限
157
- // (maxFiles 10000)一次就是 1 万元素的 IN,`?limit=100` 则是 100 次这样的串行往返。PG 的 65535
158
- // 参数协议墙虽未触到,但 1 万元素 IN 本身对计划器/网络包就是坏形。1000/块是保守值。
159
78
  const CHUNK = 1000;
160
79
  const out = new Map();
161
80
  for (let i = 0; i < distinct.length; i += CHUNK) {
@@ -175,10 +94,6 @@ export async function sqlBlobSizes(dialect, pool, hashes) {
175
94
  }
176
95
  return out;
177
96
  }
178
- /** SHARED index op — DELETE the index rows for `hashes` that have passed the grace window (created_at < cutoff). Returns
179
- * the hashes ACTUALLY deleted (so the MinIO backend deletes exactly the objects whose index rows it just removed — and
180
- * a within-grace young orphan is left for a later sweep). Best-effort: a DB error is swallowed (under-delete is
181
- * harmless; the next sweep retries). The grace guard (delete-in-use protection) is IDENTICAL across both backends. */
182
97
  async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
183
98
  if (hashes.length === 0)
184
99
  return [];
@@ -186,8 +101,6 @@ async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
186
101
  const cutoff = new Date(Date.now() - BLOB_GC_GRACE_MS);
187
102
  if (dialect === "tidb") {
188
103
  const ph = distinct.map(() => "?").join(",");
189
- // MySQL/TiDB DELETE can't RETURNING — SELECT the grace-passed subset first, then DELETE it (the SELECT+DELETE
190
- // race is benign: a row that flips referenced between them just isn't deleted = under-delete = harmless).
191
104
  const grace = await pool
192
105
  .query(`SELECT blob_hash FROM snapshot_blob WHERE created_at < ? AND blob_hash IN (${ph})`, [cutoff, ...distinct])
193
106
  .then(([rows]) => rows.map((r) => String(r.blob_hash)))
@@ -206,35 +119,11 @@ async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
206
119
  .catch(() => undefined);
207
120
  return (r?.rows ?? []).map((row) => row.blob_hash);
208
121
  }
209
- /**
210
- * MinIO BlobBackend — content-addressed bytes in an S3-compatible object store, via the zero-dependency
211
- * {@link presignS3Url} SigV4 generator + `fetch` (NO S3 SDK; the repo's no-SDK posture). The object key is
212
- * `keyPrefix + hash` (content-addressed → idempotent PUT, dedup across snapshots is automatic).
213
- *
214
- * THE SQL INDEX: the bytes go to MinIO, but a `snapshot_blob` INDEX row `(blob_hash, byte_len, created_at)` with bytes
215
- * NULL is kept in SQL (same pool/dialect). This is what makes the orphan-reference computation + the created_at grace
216
- * window + the §10 presence pre-check work identically to the SQL backend (see the file header). hasBlobs reads the
217
- * index; putBlob PUTs then upserts the index (created_at-refresh on conflict); deleteBlobs deletes the grace-passed
218
- * index rows AND the matching MinIO objects (called ONLY from the async sweep reaper).
219
- *
220
- * INTEGRITY (security-critical): getBlob VERIFIES sha256(bytes) === hash before returning. A content-addressed store's
221
- * whole safety rests on bytes-match-hash; a mismatch means corruption (network) or POISONING (a hostile object written
222
- * under a hash it doesn't match) — treated as ABSENT (undefined), NEVER returning wrong bytes. This mirrors the
223
- * importManifest content-address check the SQL stores already do.
224
- */
225
- /** 鲁棒性批2 C2/C3/C6(2026-08-03):S3 兼容端点 IO 的统一 abort 上界(fleet-client C1 定常量形
226
- * 先例)。半开端点此前各挂 undici 默认 ~300s headersTimeout——blob GET 钉死 restore/session-import
227
- * 同步路径,SendUserFile 工具调用永不返回。120s 对慢链路 ~64MiB 单 PUT/GET 仍宽裕(presignTtlSec
228
- * 注释同口径),远小于病形。send-user-file 与 leader/diffup 同 import 此常量。 */
229
122
  export const OBJECT_STORE_IO_TIMEOUT_MS = 120_000;
230
123
  export class MinioBlobBackend {
231
124
  cfg;
232
125
  keyPrefix;
233
126
  ttl;
234
- /** The SQL index pool+dialect, or undefined for an index-LESS backend (the migration script + the unit suite, which
235
- * exercise ONLY the object I/O — put/get/has-by-HEAD/delete — with no DB). When undefined, hasBlobs falls back to a
236
- * HEAD-per-hash probe and deleteBlobs ignores the grace window (the script is a stop-the-world copy, not the live GC
237
- * path). The file-snapshot stores ALWAYS pass the pool, so the live reference-tracking always has the index. */
238
127
  indexPool;
239
128
  indexDialect;
240
129
  constructor(cfg, index) {
@@ -250,8 +139,6 @@ export class MinioBlobBackend {
250
139
  return `${this.keyPrefix}${hash}`;
251
140
  }
252
141
  presign(hash, method) {
253
- // presignS3Url only declares GET|PUT in its param type, but the SigV4 canonicalization is identical for any verb
254
- // (host-only SignedHeaders, UNSIGNED-PAYLOAD) — HEAD/DELETE sign correctly with the same string math. Cast the verb.
255
142
  return presignS3Url({
256
143
  endpoint: this.cfg.endpoint,
257
144
  bucket: this.cfg.bucket,
@@ -263,26 +150,13 @@ export class MinioBlobBackend {
263
150
  expiresSec: this.ttl,
264
151
  });
265
152
  }
266
- /** PUT the bytes under the content-addressed key, THEN upsert the SQL INDEX row (bytes NULL, created_at refresh on
267
- * conflict — the SAME grace discipline as SqlBlobBackend, so a just-PUT blob an in-flight snapshot needs is touched=
268
- * now and protected). Idempotent (same hash ⇒ overwrite-with-identical-bytes + created_at bump). A non-2xx PUT throws
269
- * (an upload failure must surface — a silent drop would later read_failed on restore). The PUT happens BEFORE the
270
- * index upsert so an index row never claims a blob the object store doesn't hold (an index row with no object would
271
- * let hasBlobs report a phantom present; the reverse — an object with no index — is harmless and pruned by a bucket
272
- * lifecycle if it ever happens). NOTE no post-PUT read-back verify on the LIVE path (perf): content-addressing
273
- * catches a corrupted PUT at the next getBlob sha-check (fail-closed); the one-time migration script DOES read-back. */
274
153
  async putBlob(hash, bytes) {
275
154
  const url = this.presign(hash, "PUT");
276
- // C2(2026-08-03):走注入座(此前直用全局 fetch=测试桩接不进的站点)+ abort 上界(半开端点
277
- // 此前挂 undici ~300s;120s 对慢链路大对象仍宽裕,presignTtlSec 注释同口径)。六站点同批。
278
155
  const res = await (this.cfg.fetchImpl ?? fetch)(url, { method: "PUT", body: bytes, signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
279
156
  if (!res.ok) {
280
157
  const detail = await res.text().catch(() => "");
281
158
  throw new Error(`MinioBlobBackend.putBlob ${hash}: HTTP ${res.status} ${detail.slice(0, 200)}`);
282
159
  }
283
- // Upsert the bytes-NULL INDEX row (byte_len + created_at) so the orphan/grace reference-tracking + presence checks
284
- // see this blob. Same created_at-refresh-on-conflict as the SQL backend. (No index pool ⇒ object-only mode = the
285
- // migration/unit path; the live stores always pass it.)
286
160
  if (this.indexPool && this.indexDialect) {
287
161
  if (this.indexDialect === "tidb") {
288
162
  await this.indexPool.query("INSERT INTO snapshot_blob (blob_hash, byte_len, bytes, created_at) VALUES (?,?,NULL,?) ON DUPLICATE KEY UPDATE created_at = VALUES(created_at)", [hash, bytes.byteLength, new Date()]);
@@ -292,12 +166,6 @@ export class MinioBlobBackend {
292
166
  }
293
167
  }
294
168
  }
295
- /** GET the bytes for `hash`, then VERIFY content-address (sha256(bytes)===hash). THREE branches (finding 11):
296
- * - 404 → undefined (genuinely ABSENT — the file-snapshot store treats undefined as a missing blob).
297
- * - sha256 MISMATCH → undefined (corruption/poisoning; treat corrupt-as-absent, fail-CLOSED, NEVER wrong bytes).
298
- * - a NON-2xx-NON-404 (5xx/throttle) OR a network/fetch REJECT → THROW (a TRANSIENT fault must fail LOUD so the
299
- * restore/import surfaces an error, NOT silently drop a file as "missing"; the SQL backend likewise propagates a
300
- * pool error). The snapshot store's restore() wraps applyManifest so this throw becomes a {ok:false} restore_failed. */
301
169
  async getBlob(hash) {
302
170
  const url = this.presign(hash, "GET");
303
171
  let res;
@@ -308,24 +176,19 @@ export class MinioBlobBackend {
308
176
  throw new Error(`MinioBlobBackend.getBlob ${hash}: transient fetch failure: ${err.message}`);
309
177
  }
310
178
  if (res.status === 404)
311
- return undefined; // genuinely absent
179
+ return undefined;
312
180
  if (!res.ok) {
313
181
  const detail = await res.text().catch(() => "");
314
182
  throw new Error(`MinioBlobBackend.getBlob ${hash}: transient HTTP ${res.status} ${detail.slice(0, 200)}`);
315
183
  }
316
184
  const buf = new Uint8Array(await res.arrayBuffer());
317
- // content-address integrity: the bytes MUST hash to the key. A mismatch = corrupt/poisoned → undefined (absent), NOT wrong bytes.
318
185
  if (createHash("sha256").update(buf).digest("hex") !== hash)
319
186
  return undefined;
320
187
  return buf;
321
188
  }
322
- /** The subset of `hashes` present — reads the SQL INDEX (cheaper than a HEAD-per-hash AND consistent with the SQL
323
- * backend). Falls back to a presigned HEAD per hash ONLY in object-only mode (no index pool: the migration / unit
324
- * path). The caller passes DISTINCT hashes (migration idempotency batch / reap orphan set / §10 presence pre-check). */
325
189
  async hasBlobs(hashes) {
326
190
  if (this.indexPool && this.indexDialect)
327
191
  return sqlHasBlobs(this.indexDialect, this.indexPool, hashes);
328
- // object-only mode (migration/unit): HEAD probe, bounded concurrency.
329
192
  const distinct = [...new Set(hashes)];
330
193
  const present = new Set();
331
194
  await mapBounded(distinct, MINIO_OP_CONCURRENCY, async (hash) => {
@@ -335,22 +198,13 @@ export class MinioBlobBackend {
335
198
  present.add(hash);
336
199
  }
337
200
  catch {
338
- /* treat an unreachable HEAD as absent — a false-absent only re-PUTs (idempotent) or re-checks; never a false-present */
339
201
  }
340
202
  });
341
203
  return present;
342
204
  }
343
- /** D-1 孤儿对象 GC(clay 拍 2026-07-28):列举本后端 keyPrefix 下的全部对象(ListObjectsV2 分页,
344
- * 零依赖 XML 取 <Key>/<LastModified>/<NextContinuationToken>)。**server 端 sweep 专用**——presign 的
345
- * bucket-GET 列举能力绝不外发。响应非 2xx 即 throw(sweep 调用方自 catch:列举失败=本轮跳过,
346
- * 绝不把「列不出来」当「没有孤儿」)。 */
347
205
  async *listObjects() {
348
206
  const f = this.cfg.fetchImpl ?? fetch;
349
207
  let token;
350
- // 🔴 复审 F3:分页上限。异常/代理后端回环或恒定的 continuation-token 会让本循环无界翻页
351
- // (实测可翻 5001 页仍不停),叠加维护 tick 就是逐轮堆积。超限 fail-loud —— 与本函数既有的
352
- // 「列不出来 ≠ 没有孤儿」口径一致(宁可整轮失败也不给出残缺的「孤儿全集」)。
353
- // 1000 页 × 1000 键/页 = 100 万对象,远超任何真实附件语料;真撞上=后端异常而非规模问题。
354
208
  const MAX_PAGES = 1000;
355
209
  let pages = 0;
356
210
  do {
@@ -371,15 +225,14 @@ export class MinioBlobBackend {
371
225
  if (!res.ok)
372
226
  throw new Error(`listObjects: MinIO ${res.status}`);
373
227
  const xml = await res.text();
374
- // <Contents><Key>k</Key>…<LastModified>ISO</LastModified>…</Contents> —— 逐块取,键序无假设。
375
228
  for (const m of xml.matchAll(/<Contents>([\s\S]*?)<\/Contents>/g)) {
376
229
  const block = m[1];
377
230
  const key = /<Key>([^<]*)<\/Key>/.exec(block)?.[1];
378
231
  const lm = /<LastModified>([^<]*)<\/LastModified>/.exec(block)?.[1];
379
232
  if (key === undefined || !key.startsWith(this.keyPrefix))
380
- continue; // prefix 外键(理论不可达)跳过
233
+ continue;
381
234
  const ms = lm ? Date.parse(lm) : NaN;
382
- yield { hash: key.slice(this.keyPrefix.length), lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() }; // 坏时间戳=按「新」处理(grace 保护,宁漏勿误删)
235
+ yield { hash: key.slice(this.keyPrefix.length), lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
383
236
  }
384
237
  const truncated = /<IsTruncated>true<\/IsTruncated>/.test(xml);
385
238
  token = truncated ? /<NextContinuationToken>([^<]*)<\/NextContinuationToken>/.exec(xml)?.[1] : undefined;
@@ -387,8 +240,6 @@ export class MinioBlobBackend {
387
240
  throw new Error("listObjects: truncated response without NextContinuationToken");
388
241
  } while (token !== undefined);
389
242
  }
390
- /** 单对象 HEAD 探针(复审 B-F1 的删前复查面):返回当前 LastModified(缺席=对象已不在)。
391
- * put 会 PUT 同键 ⇒ 刷新 LastModified,所以「删前再 HEAD 一次」能看见「刚刚被重新引用」的对象。 */
392
243
  async headObject(hash) {
393
244
  const f = this.cfg.fetchImpl ?? fetch;
394
245
  const res = await f(this.presign(hash, "HEAD"), { method: "HEAD", signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
@@ -396,28 +247,17 @@ export class MinioBlobBackend {
396
247
  return undefined;
397
248
  const lm = res.headers.get("last-modified");
398
249
  const ms = lm ? Date.parse(lm) : NaN;
399
- return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() }; // 无/坏时间戳=按「新」处理(宁漏勿误删)
250
+ return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
400
251
  }
401
- /** Byte-delete the given hashes — called ONLY from the async sweepOrphanBlobs reaper (NEVER the synchronous purge
402
- * path). Honors the SAME created_at grace window as the SQL backend: with an index, DELETE the grace-passed index
403
- * rows first (the index's created_at is the authoritative age — an S3 object carries no service-controlled timestamp
404
- * the backend reads), then delete the MinIO objects ONLY for those grace-passed hashes (so a just-PUT blob an
405
- * in-flight snapshot needs is NOT delete-in-use). ERROR-TOLERANT + BOUNDED: each object delete is independent (a
406
- * failure is skipped → retried next sweep), at most MINIO_OP_CONCURRENCY in flight (no FD/throttle storm). In
407
- * object-only mode (no index pool: the migration/unit cleanup path) it deletes every given hash with no grace. */
408
252
  async deleteBlobs(hashes) {
409
253
  if (hashes.length === 0)
410
254
  return 0;
411
- // Determine which hashes are grace-passed AND remove their index rows (so the index no longer claims them present).
412
- // Order: index-row DELETE FIRST, then the object DELETE — if the object delete fails, the orphan re-appears only as
413
- // an object with no index, which a later sweep can't re-target; acceptable (under-delete is harmless, and a bucket
414
- // lifecycle rule prunes index-less objects). Doing it index-first keeps hasBlobs honest immediately.
415
255
  let toDelete;
416
256
  if (this.indexPool && this.indexDialect) {
417
257
  toDelete = await sqlDeleteIndexRowsPastGrace(this.indexDialect, this.indexPool, hashes);
418
258
  }
419
259
  else {
420
- toDelete = [...new Set(hashes)]; // object-only mode (migration/unit cleanup): no grace, delete all given
260
+ toDelete = [...new Set(hashes)];
421
261
  }
422
262
  if (toDelete.length === 0)
423
263
  return 0;
@@ -425,16 +265,13 @@ export class MinioBlobBackend {
425
265
  await mapBounded(toDelete, MINIO_OP_CONCURRENCY, async (hash) => {
426
266
  try {
427
267
  const res = await (this.cfg.fetchImpl ?? fetch)(this.presign(hash, "DELETE"), { method: "DELETE", signal: AbortSignal.timeout(OBJECT_STORE_IO_TIMEOUT_MS) });
428
- // 🔴 复审 F4:统计**真实**删除数——index-less(附件)分支此前恒返回入参长度,MinIO 完全不可写
429
- // 时指标/日志照报「清理成功」。2xx/404(已不在)都算删成功;其余=失败,不计数。
430
268
  if (res.ok || res.status === 404)
431
269
  okCount++;
432
270
  }
433
271
  catch {
434
- /* error-tolerant: a failed/timed-out object delete is skipped → retried next sweep (eventual consistency) */
435
272
  }
436
273
  });
437
- return okCount; // the index rows we removed = the work done (best-effort object deletes follow async)
274
+ return okCount;
438
275
  }
439
276
  }
440
277
  //# sourceMappingURL=blob-backend.js.map
@@ -1,7 +1,4 @@
1
1
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
2
- /** PG translation of the tidb-pool.ts `circuit_breaker` DDL (DATETIME(3)→TIMESTAMPTZ(3)). Disjoint from other
3
- * stores' tables, so a self-contained ensureSchema is safe (no central-schema shadowing). TiDB's DDL lives
4
- * centrally in tidb-pool.ts (no PG-side twin needed there — production wires ONE ensureTidbSchema). */
5
2
  export const PG_BREAKER_STATE_SCHEMA = [
6
3
  `CREATE TABLE IF NOT EXISTS circuit_breaker (
7
4
  breaker_key VARCHAR(190) COLLATE "C" NOT NULL,
@@ -12,29 +9,21 @@ export const PG_BREAKER_STATE_SCHEMA = [
12
9
  PRIMARY KEY (breaker_key)
13
10
  )`,
14
11
  ];
15
- /** Idempotent schema apply for the breaker table (the PG twin of tidb-pool.ts ensureSchema, scoped to this store). */
16
12
  export async function ensureSchema(pool) {
17
13
  for (const stmt of PG_BREAKER_STATE_SCHEMA)
18
14
  await pool.query(stmt);
19
15
  }
20
- /** Dual-dialect cross-replica BreakerState. See the file header for the dialect-delta ledger. */
21
16
  export class SqlBreakerState {
22
17
  db;
23
18
  onWriteFail;
24
19
  cache = new Map();
25
20
  timer;
26
- /** Keys with an in-flight write-through upsert. A concurrent `refresh()` must NOT overwrite or prune
27
- * these — their newer local value may not be in the DB yet. Closes the set()-vs-refresh race (core
28
- * 1.40.0 council finding #5). */
29
21
  pending = new Set();
30
22
  writeFailStreak = 0;
31
- constructor(db,
32
- /** LOW (SILENT-FALLBACK P1): write-through failure tap — streak per call, 0 on the recovery edge. */
33
- onWriteFail) {
23
+ constructor(db, onWriteFail) {
34
24
  this.db = db;
35
25
  this.onWriteFail = onWriteFail;
36
26
  }
37
- /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
38
27
  q(tidb, pg) {
39
28
  return this.db.dialect === "tidb" ? tidb : pg;
40
29
  }
@@ -42,9 +31,9 @@ export class SqlBreakerState {
42
31
  return this.cache.get(key);
43
32
  }
44
33
  set(key, snap) {
45
- this.cache.set(key, snap); // synchronous: the local breaker hot path sees it immediately
46
- this.pending.add(key); // guard the in-flight write from a concurrent refresh
47
- void this.writeThrough(key, snap); // async, best-effort; never throws into the caller
34
+ this.cache.set(key, snap);
35
+ this.pending.add(key);
36
+ void this.writeThrough(key, snap);
48
37
  }
49
38
  async writeThrough(key, snap) {
50
39
  try {
@@ -57,41 +46,18 @@ export class SqlBreakerState {
57
46
  try {
58
47
  this.onWriteFail?.(0);
59
48
  }
60
- catch { /* observability never breaks advisory state */ }
61
- } // the new value is now in the DB → refresh may reconcile this key again
62
- // KNOWN/ACCEPTED (audit B): two rapid set()s on the SAME key race their async writeThroughs — the
63
- // older write can land last (no version column), so a later refresh() may briefly revert this
64
- // replica to the older snapshot (e.g. re-close a just-opened breaker). Advisory state: the very
65
- // next failure re-opens it locally (set() is synchronous on the hot path) and re-publishes. A
66
- // monotonic guard would need versioned rows + CAS; not worth it for an advisory cache.
49
+ catch { }
50
+ }
67
51
  }
68
52
  catch {
69
53
  try {
70
54
  this.onWriteFail?.(++this.writeFailStreak);
71
55
  }
72
- catch { /* ditto */ } // LOW: previously a bare swallow
73
- // Advisory state — swallow; a DB outage must not turn into a brain-call failure. Crucially we KEEP
74
- // `pending` on failure: the DB still holds the OLD value, so releasing the guard here would let the
75
- // next refresh() clobber this replica's just-set (correct) local value — silently resetting an open
76
- // breaker toward a known-bad backend. The guard self-heals on the next successful set()/write.
56
+ catch { }
77
57
  }
78
58
  }
79
- /**
80
- * Pull the shared table into the local cache so this replica learns peers' trips, and PRUNE keys that
81
- * vanished from the DB — but never touch a locally-pending write (its upsert may not have landed yet),
82
- * else a just-`set` trip would be clobbered/reverted before it reaches the DB. Eventually consistent:
83
- * a replica is at most one refresh interval behind a peer, never wrong in a data-losing way.
84
- * `halfOpenInFlight` is intentionally not shared (it is a per-replica probe counter).
85
- *
86
- * SELECT text is byte-identical on both dialects (no placeholders) — written once, not through `q()`
87
- * (same precedent as image-bake-store-sql.ts's queued-bake lookup).
88
- */
89
59
  async refresh() {
90
60
  try {
91
- // Snapshot the pending set BEFORE the read: a key whose write-through is in-flight when the SELECT starts may
92
- // not be in the SELECT's snapshot yet, and its writeThrough can `pending.delete` DURING the await — so a
93
- // check-after-read (`this.pending.has`) alone would let the stale row clobber the just-set local value. Skip a
94
- // key pending NOW or at read-start (union) to actually close the set()-vs-refresh race (#5), not just narrow it.
95
61
  const pendingAtStart = new Set(this.pending);
96
62
  const guarded = (key) => this.pending.has(key) || pendingAtStart.has(key);
97
63
  const { rows } = await this.db.query("SELECT breaker_key, phase, failures, opened_at FROM circuit_breaker");
@@ -100,7 +66,7 @@ export class SqlBreakerState {
100
66
  const key = String(r.breaker_key);
101
67
  seen.add(key);
102
68
  if (guarded(key))
103
- continue; // don't clobber a locally-pending newer value (#5)
69
+ continue;
104
70
  this.cache.set(key, {
105
71
  phase: r.phase,
106
72
  failures: Number(r.failures),
@@ -112,10 +78,8 @@ export class SqlBreakerState {
112
78
  this.cache.delete(key);
113
79
  }
114
80
  catch {
115
- // Keep the last-known cache on a transient read failure (fail static, don't blank the breaker).
116
81
  }
117
82
  }
118
- /** Start the periodic refresh loop. `unref` so it never holds the process open. Returns `this`. */
119
83
  startRefresh(intervalMs = 5_000) {
120
84
  this.timer ??= setInterval(() => void this.refresh(), intervalMs);
121
85
  this.timer.unref?.();
@@ -127,13 +91,11 @@ export class SqlBreakerState {
127
91
  this.timer = undefined;
128
92
  }
129
93
  }
130
- /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
131
94
  export class TiDBBreakerState extends SqlBreakerState {
132
95
  constructor(pool, onWriteFail) {
133
96
  super(mysqlDriver(pool), onWriteFail);
134
97
  }
135
98
  }
136
- /** PostgreSQL binding — historical class name + ctor shape preserved. */
137
99
  export class PgBreakerState extends SqlBreakerState {
138
100
  constructor(pool, onWriteFail) {
139
101
  super(pgDriver(pool), onWriteFail);