@sema-agent/server 1.323.0 → 2.0.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 (539) hide show
  1. package/dist/approval-hmac.d.ts +40 -0
  2. package/dist/approval-hmac.js +67 -0
  3. package/dist/approval.d.ts +31 -0
  4. package/dist/approval.js +72 -4
  5. package/dist/audit.d.ts +26 -0
  6. package/dist/audit.js +40 -1
  7. package/dist/auth-bridge.d.ts +10 -0
  8. package/dist/auth-bridge.js +47 -4
  9. package/dist/auth-keys.d.ts +19 -0
  10. package/dist/auth-keys.js +13 -0
  11. package/dist/bake-runner/main.d.ts +5 -0
  12. package/dist/bake-runner/main.js +43 -4
  13. package/dist/bake-runner/protocol.d.ts +57 -0
  14. package/dist/bake-runner/protocol.js +58 -3
  15. package/dist/bake-runner/runner.d.ts +54 -0
  16. package/dist/bake-runner/runner.js +87 -5
  17. package/dist/bench/l8/artifact.d.ts +63 -0
  18. package/dist/bench/l8/artifact.js +61 -2
  19. package/dist/bench/l8/escape.d.ts +36 -0
  20. package/dist/bench/l8/escape.js +25 -0
  21. package/dist/bench/l8/index.d.ts +14 -0
  22. package/dist/bench/l8/index.js +14 -0
  23. package/dist/bench/l8/probes.d.ts +74 -0
  24. package/dist/bench/l8/probes.js +60 -0
  25. package/dist/bench/l8/run-probes.d.ts +43 -0
  26. package/dist/bench/l8/run-probes.js +45 -2
  27. package/dist/bench/s1/arms.d.ts +126 -0
  28. package/dist/bench/s1/arms.js +188 -13
  29. package/dist/bench/s1/live-deps.d.ts +66 -0
  30. package/dist/bench/s1/live-deps.js +249 -11
  31. package/dist/bench/s1/oracle.d.ts +49 -0
  32. package/dist/bench/s1/oracle.js +36 -1
  33. package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
  34. package/dist/bench/s1/repair-oracle-adapter.js +30 -0
  35. package/dist/bench/s1/reviewer.d.ts +38 -0
  36. package/dist/bench/s1/reviewer.js +34 -0
  37. package/dist/bench/s1/row.d.ts +77 -0
  38. package/dist/bench/s1/row.js +62 -0
  39. package/dist/bench/s1/run-firm.d.ts +47 -0
  40. package/dist/bench/s1/run-firm.js +70 -4
  41. package/dist/bench/s1/runner-ctx.d.ts +71 -0
  42. package/dist/bench/s1/runner-ctx.js +36 -0
  43. package/dist/bench/s1/tasks.d.ts +108 -0
  44. package/dist/bench/s1/tasks.js +115 -0
  45. package/dist/boot-reclaim.d.ts +19 -0
  46. package/dist/boot-reclaim.js +20 -0
  47. package/dist/brain.d.ts +25 -0
  48. package/dist/brain.js +61 -1
  49. package/dist/budget.d.ts +66 -0
  50. package/dist/budget.js +128 -4
  51. package/dist/capabilities/builtin-tools.d.ts +3 -0
  52. package/dist/capabilities/builtin-tools.js +3 -0
  53. package/dist/capabilities/center-plugins.d.ts +17 -1
  54. package/dist/capabilities/center-plugins.js +45 -4
  55. package/dist/capabilities/center-prompts.d.ts +46 -0
  56. package/dist/capabilities/center-prompts.js +57 -3
  57. package/dist/capabilities/code-review-council.d.ts +28 -0
  58. package/dist/capabilities/code-review-council.js +40 -7
  59. package/dist/capabilities/collab-wire.d.ts +2 -0
  60. package/dist/capabilities/collab-workflows.d.ts +7 -0
  61. package/dist/capabilities/collab-workflows.js +44 -1
  62. package/dist/capabilities/oa-tools.d.ts +6 -0
  63. package/dist/capabilities/oa-tools.js +10 -0
  64. package/dist/capabilities/prompt.d.ts +14 -0
  65. package/dist/capabilities/prompt.js +14 -0
  66. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  67. package/dist/capabilities/prompts/code-review.js +14 -0
  68. package/dist/capabilities/prompts/identity.d.ts +6 -0
  69. package/dist/capabilities/prompts/identity.js +6 -0
  70. package/dist/capabilities/prompts/team.d.ts +4 -0
  71. package/dist/capabilities/prompts/team.js +4 -0
  72. package/dist/capabilities/repo-tools.d.ts +11 -0
  73. package/dist/capabilities/repo-tools.js +8 -0
  74. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  75. package/dist/capabilities/sandbox-file-send.js +91 -3
  76. package/dist/capabilities/scenarios.d.ts +130 -0
  77. package/dist/capabilities/scenarios.js +180 -7
  78. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  79. package/dist/capabilities/select-environment-tool.js +30 -0
  80. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  81. package/dist/capabilities/send-user-file-tool.js +65 -1
  82. package/dist/capabilities/skills.d.ts +29 -0
  83. package/dist/capabilities/skills.js +13 -2
  84. package/dist/capabilities/team.d.ts +22 -0
  85. package/dist/capabilities/team.js +22 -2
  86. package/dist/capabilities/tool-defer.d.ts +26 -0
  87. package/dist/capabilities/tool-defer.js +3 -0
  88. package/dist/config-center/apply-effective.d.ts +65 -0
  89. package/dist/config-center/apply-effective.js +204 -12
  90. package/dist/config-center/http-client.d.ts +34 -0
  91. package/dist/config-center/http-client.js +68 -0
  92. package/dist/config-center/restart-signal.d.ts +22 -0
  93. package/dist/config-center/restart-signal.js +39 -0
  94. package/dist/config-center/skills-mcp.d.ts +20 -1
  95. package/dist/config-center/skills-mcp.js +42 -7
  96. package/dist/config-center/types.d.ts +92 -1
  97. package/dist/config-lkg.d.ts +12 -0
  98. package/dist/config-lkg.js +41 -0
  99. package/dist/config-provider.d.ts +78 -0
  100. package/dist/config-provider.js +165 -2
  101. package/dist/config-types.d.ts +546 -0
  102. package/dist/config.d.ts +64 -0
  103. package/dist/config.js +455 -60
  104. package/dist/degenerate-instrument.d.ts +38 -0
  105. package/dist/degenerate-instrument.js +55 -3
  106. package/dist/elicitation.d.ts +39 -0
  107. package/dist/elicitation.js +87 -3
  108. package/dist/env-facts.d.ts +61 -0
  109. package/dist/env-facts.js +73 -7
  110. package/dist/fleet/fleet-bus.d.ts +185 -0
  111. package/dist/fleet/fleet-bus.js +333 -27
  112. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  113. package/dist/fleet/subagent-tail-bus.js +33 -2
  114. package/dist/fleet-client.d.ts +38 -0
  115. package/dist/fleet-client.js +44 -9
  116. package/dist/fleet-lease.d.ts +46 -0
  117. package/dist/fleet-lease.js +73 -5
  118. package/dist/hooks/branch-transcript.d.ts +8 -0
  119. package/dist/hooks/branch-transcript.js +33 -0
  120. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  121. package/dist/hooks/cc-stop-prompt.js +43 -1
  122. package/dist/hooks/hook-llm.d.ts +15 -0
  123. package/dist/hooks/hook-llm.js +47 -0
  124. package/dist/hooks/hook-runner.d.ts +89 -0
  125. package/dist/hooks/hook-runner.js +327 -18
  126. package/dist/http/idempotency.d.ts +31 -0
  127. package/dist/http/idempotency.js +34 -0
  128. package/dist/http/principal-gate.d.ts +17 -0
  129. package/dist/http/principal-gate.js +35 -3
  130. package/dist/http/route-ctx.d.ts +36 -0
  131. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  132. package/dist/http/routes/approvals-assistant.js +253 -23
  133. package/dist/http/routes/attachments.js +22 -4
  134. package/dist/http/routes/capabilities.d.ts +7 -0
  135. package/dist/http/routes/capabilities.js +202 -1
  136. package/dist/http/routes/fleet.d.ts +6 -0
  137. package/dist/http/routes/fleet.js +94 -5
  138. package/dist/http/routes/images.d.ts +17 -0
  139. package/dist/http/routes/images.js +222 -14
  140. package/dist/http/routes/leader.d.ts +7 -0
  141. package/dist/http/routes/leader.js +10 -0
  142. package/dist/http/routes/memory-policy.d.ts +6 -0
  143. package/dist/http/routes/memory-policy.js +38 -6
  144. package/dist/http/routes/notify-wake.d.ts +10 -0
  145. package/dist/http/routes/notify-wake.js +19 -3
  146. package/dist/http/routes/observability.d.ts +6 -0
  147. package/dist/http/routes/observability.js +19 -2
  148. package/dist/http/routes/runs.d.ts +15 -0
  149. package/dist/http/routes/runs.js +464 -41
  150. package/dist/http/routes/session-sync.d.ts +15 -0
  151. package/dist/http/routes/session-sync.js +229 -19
  152. package/dist/http/routes/sessions-list.d.ts +6 -0
  153. package/dist/http/routes/sessions-list.js +35 -4
  154. package/dist/http/routes/sessions.d.ts +14 -0
  155. package/dist/http/routes/sessions.js +176 -31
  156. package/dist/http/routes/side-query.d.ts +8 -0
  157. package/dist/http/routes/side-query.js +26 -1
  158. package/dist/http/routes/tasks.d.ts +18 -0
  159. package/dist/http/routes/tasks.js +501 -38
  160. package/dist/http/routes/trace-usage.d.ts +10 -0
  161. package/dist/http/routes/trace-usage.js +66 -14
  162. package/dist/http/routes/workflows.d.ts +12 -0
  163. package/dist/http/routes/workflows.js +180 -19
  164. package/dist/http/run-meta.d.ts +11 -0
  165. package/dist/http/run-meta.js +6 -0
  166. package/dist/http/send.d.ts +19 -0
  167. package/dist/http/send.js +11 -0
  168. package/dist/http/server.d.ts +255 -0
  169. package/dist/http/server.js +921 -75
  170. package/dist/http/sse-log.d.ts +15 -0
  171. package/dist/http/sse-log.js +16 -0
  172. package/dist/http/tar.d.ts +9 -0
  173. package/dist/http/tar.js +21 -5
  174. package/dist/http/wire-gate.d.ts +9 -0
  175. package/dist/http/wire-gate.js +9 -0
  176. package/dist/http/wire-types.d.ts +187 -0
  177. package/dist/http/workspace-content.d.ts +8 -0
  178. package/dist/http/workspace-content.js +10 -0
  179. package/dist/images/bake-validate.d.ts +63 -0
  180. package/dist/images/bake-validate.js +71 -1
  181. package/dist/images/manifest.d.ts +19 -0
  182. package/dist/images/manifest.js +6 -1
  183. package/dist/index.d.ts +1 -5
  184. package/dist/index.js +10 -5
  185. package/dist/key-resolver.d.ts +23 -0
  186. package/dist/key-resolver.js +27 -2
  187. package/dist/leader/diffout.d.ts +23 -0
  188. package/dist/leader/diffout.js +8 -1
  189. package/dist/leader/diffup.d.ts +32 -0
  190. package/dist/leader/diffup.js +43 -0
  191. package/dist/leader/endpoint.d.ts +20 -0
  192. package/dist/leader/endpoint.js +25 -2
  193. package/dist/leader/fanout.d.ts +60 -0
  194. package/dist/leader/fanout.js +29 -3
  195. package/dist/leader/grader-env-factory.d.ts +74 -0
  196. package/dist/leader/grader-env-factory.js +62 -3
  197. package/dist/leader/leader.d.ts +97 -0
  198. package/dist/leader/leader.js +151 -4
  199. package/dist/leader/merge.d.ts +94 -0
  200. package/dist/leader/merge.js +112 -9
  201. package/dist/leader/planner.d.ts +52 -0
  202. package/dist/leader/planner.js +47 -3
  203. package/dist/leader/repair-oracle.d.ts +61 -0
  204. package/dist/leader/repair-oracle.js +59 -1
  205. package/dist/leader/repair-wire.d.ts +78 -0
  206. package/dist/leader/repair-wire.js +79 -2
  207. package/dist/leader/wire.d.ts +79 -0
  208. package/dist/leader/wire.js +255 -8
  209. package/dist/lsp/e2b-bridge.d.ts +16 -0
  210. package/dist/lsp/e2b-bridge.js +32 -4
  211. package/dist/lsp/e2b-manager.d.ts +10 -0
  212. package/dist/lsp/e2b-manager.js +18 -2
  213. package/dist/lsp/lsp-frames.d.ts +12 -0
  214. package/dist/lsp/lsp-frames.js +12 -0
  215. package/dist/lsp/manager.d.ts +12 -0
  216. package/dist/lsp/manager.js +35 -2
  217. package/dist/lsp/types.d.ts +10 -0
  218. package/dist/lsp/ws-transport.d.ts +12 -0
  219. package/dist/lsp/ws-transport.js +53 -5
  220. package/dist/lsp-evict.d.ts +14 -0
  221. package/dist/lsp-evict.js +15 -1
  222. package/dist/main.js +2132 -96
  223. package/dist/memory-export.d.ts +1 -0
  224. package/dist/memory-export.js +4 -0
  225. package/dist/memory-scope.d.ts +49 -0
  226. package/dist/memory-scope.js +87 -4
  227. package/dist/memory-sync-client.d.ts +24 -0
  228. package/dist/memory-sync-client.js +41 -2
  229. package/dist/memory-sync.d.ts +23 -0
  230. package/dist/memory-sync.js +70 -1
  231. package/dist/model-select.d.ts +39 -0
  232. package/dist/model-select.js +42 -1
  233. package/dist/observability/cost-quota.d.ts +20 -0
  234. package/dist/observability/cost-quota.js +4 -0
  235. package/dist/observability/cost-taxonomy.d.ts +45 -0
  236. package/dist/observability/cost-taxonomy.js +34 -0
  237. package/dist/observability/logger.d.ts +1 -0
  238. package/dist/observability/logger.js +6 -0
  239. package/dist/observability/metrics.d.ts +13 -0
  240. package/dist/observability/metrics.js +63 -0
  241. package/dist/observability/otel-exporter.d.ts +9 -0
  242. package/dist/observability/otel-exporter.js +11 -3
  243. package/dist/observability/principal-context.d.ts +9 -0
  244. package/dist/observability/principal-context.js +9 -0
  245. package/dist/observability/prompt-manifest.d.ts +37 -0
  246. package/dist/observability/prompt-manifest.js +35 -1
  247. package/dist/observability/rate-limit.d.ts +10 -0
  248. package/dist/observability/rate-limit.js +1 -0
  249. package/dist/observability/tool-trace.d.ts +24 -0
  250. package/dist/observability/tool-trace.js +41 -1
  251. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  252. package/dist/orchestration/hardened-vm-runner.js +119 -4
  253. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  254. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  255. package/dist/orchestration/hardened-vm-worker.js +27 -0
  256. package/dist/orchestration/subagent-steer.d.ts +44 -0
  257. package/dist/orchestration/subagent-steer.js +45 -1
  258. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  259. package/dist/orchestration/workflow-agent-steer.js +80 -1
  260. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  261. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  262. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  263. package/dist/orchestration/workflow-notify-journal.js +187 -13
  264. package/dist/parked-decide.d.ts +61 -0
  265. package/dist/parked-decide.js +58 -1
  266. package/dist/per-task-image.d.ts +54 -0
  267. package/dist/per-task-image.js +41 -0
  268. package/dist/plan-cache-probe.d.ts +35 -0
  269. package/dist/plan-cache-probe.js +18 -3
  270. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  271. package/dist/plugins/approval-exemption-store.js +28 -2
  272. package/dist/plugins/approval-store-sql.d.ts +64 -0
  273. package/dist/plugins/approval-store-sql.js +32 -0
  274. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  275. package/dist/plugins/background-agent-store-sql.js +91 -4
  276. package/dist/plugins/background-shell-support.d.ts +98 -0
  277. package/dist/plugins/background-shell-support.js +117 -13
  278. package/dist/plugins/blob-backend.d.ts +104 -0
  279. package/dist/plugins/blob-backend.js +163 -6
  280. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  281. package/dist/plugins/breaker-state-sql.js +46 -8
  282. package/dist/plugins/caching-session-store.d.ts +42 -0
  283. package/dist/plugins/caching-session-store.js +63 -3
  284. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  285. package/dist/plugins/checkpoint-store-sql.js +239 -11
  286. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  287. package/dist/plugins/file-outcome-sink.js +9 -0
  288. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  289. package/dist/plugins/file-resume-anchor-store.js +43 -4
  290. package/dist/plugins/file-run-store.d.ts +90 -0
  291. package/dist/plugins/file-run-store.js +145 -13
  292. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  293. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  294. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  295. package/dist/plugins/file-workflow-journal-store.js +10 -0
  296. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  297. package/dist/plugins/fork-routing-session-store.js +51 -3
  298. package/dist/plugins/host-platform.d.ts +69 -0
  299. package/dist/plugins/host-platform.js +81 -2
  300. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  301. package/dist/plugins/image-bake-store-sql.js +241 -7
  302. package/dist/plugins/image-index-sql.d.ts +46 -0
  303. package/dist/plugins/image-index-sql.js +113 -4
  304. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  305. package/dist/plugins/k8s-bg-scripts.js +88 -4
  306. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  307. package/dist/plugins/k8s-exec-protocol.js +41 -0
  308. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  309. package/dist/plugins/local-checkpoint-store.js +86 -7
  310. package/dist/plugins/local-session-store.d.ts +157 -0
  311. package/dist/plugins/local-session-store.js +289 -34
  312. package/dist/plugins/local-task-attachment-store.js +14 -2
  313. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  314. package/dist/plugins/mailbox-store-sql.js +39 -6
  315. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  316. package/dist/plugins/memory-engine-pg.js +122 -4
  317. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  318. package/dist/plugins/memory-engine-tidb.js +115 -5
  319. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  320. package/dist/plugins/memory-engine-vector-util.js +5 -0
  321. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  322. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  323. package/dist/plugins/memory-run-store.d.ts +63 -0
  324. package/dist/plugins/memory-run-store.js +73 -8
  325. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  326. package/dist/plugins/memory-session-policy-store.js +17 -0
  327. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  328. package/dist/plugins/memory-sync-store-pg.js +45 -2
  329. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  330. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  331. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  332. package/dist/plugins/outcome-ledger-sql.js +97 -3
  333. package/dist/plugins/pg-approval-store.d.ts +7 -0
  334. package/dist/plugins/pg-approval-store.js +7 -0
  335. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  336. package/dist/plugins/pg-breaker-state.js +6 -0
  337. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  338. package/dist/plugins/pg-checkpoint-store.js +8 -0
  339. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  340. package/dist/plugins/pg-cost-quota.js +6 -0
  341. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  342. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  343. package/dist/plugins/pg-image-bake.d.ts +9 -0
  344. package/dist/plugins/pg-image-bake.js +9 -0
  345. package/dist/plugins/pg-image-index.d.ts +9 -0
  346. package/dist/plugins/pg-image-index.js +9 -0
  347. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  348. package/dist/plugins/pg-outcome-ledger.js +9 -0
  349. package/dist/plugins/pg-pool.d.ts +50 -0
  350. package/dist/plugins/pg-pool.js +78 -0
  351. package/dist/plugins/pg-query.d.ts +11 -0
  352. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  353. package/dist/plugins/pg-rate-limiter.js +12 -2
  354. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  355. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  356. package/dist/plugins/pg-run-store.d.ts +7 -0
  357. package/dist/plugins/pg-run-store.js +7 -0
  358. package/dist/plugins/pg-safe-json.d.ts +13 -0
  359. package/dist/plugins/pg-safe-json.js +40 -4
  360. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  361. package/dist/plugins/pg-session-policy-store.js +5 -0
  362. package/dist/plugins/pg-session-storage.d.ts +72 -0
  363. package/dist/plugins/pg-session-storage.js +152 -20
  364. package/dist/plugins/pg-session-store.d.ts +10 -0
  365. package/dist/plugins/pg-session-store.js +10 -0
  366. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  367. package/dist/plugins/pg-tool-result-store.js +7 -0
  368. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  369. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  370. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  371. package/dist/plugins/pg-workflow-run-store.js +7 -0
  372. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  373. package/dist/plugins/posix-shell-fs.js +31 -1
  374. package/dist/plugins/remote-env-adb.d.ts +25 -0
  375. package/dist/plugins/remote-env-adb.js +95 -12
  376. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  377. package/dist/plugins/remote-env-e2b.js +371 -40
  378. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  379. package/dist/plugins/remote-env-file-error.js +37 -0
  380. package/dist/plugins/remote-env-host.d.ts +151 -0
  381. package/dist/plugins/remote-env-host.js +468 -70
  382. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  383. package/dist/plugins/remote-env-k8s.js +318 -32
  384. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  385. package/dist/plugins/remote-env-local-docker.js +149 -24
  386. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  387. package/dist/plugins/remote-env-ssh.js +117 -20
  388. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  389. package/dist/plugins/remote-scratchpad.js +32 -2
  390. package/dist/plugins/remote-shell.d.ts +22 -0
  391. package/dist/plugins/remote-shell.js +27 -1
  392. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  393. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  394. package/dist/plugins/roster-store-sql.d.ts +27 -0
  395. package/dist/plugins/roster-store-sql.js +54 -0
  396. package/dist/plugins/run-store-sql.d.ts +194 -0
  397. package/dist/plugins/run-store-sql.js +210 -9
  398. package/dist/plugins/s3-presign.d.ts +20 -0
  399. package/dist/plugins/s3-presign.js +49 -2
  400. package/dist/plugins/scheduler-support.d.ts +12 -0
  401. package/dist/plugins/scheduler-support.js +75 -3
  402. package/dist/plugins/send-file-ledger.d.ts +15 -0
  403. package/dist/plugins/send-file-ledger.js +53 -4
  404. package/dist/plugins/send-user-file.d.ts +77 -0
  405. package/dist/plugins/send-user-file.js +92 -4
  406. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  407. package/dist/plugins/session-policy-store-sql.js +75 -3
  408. package/dist/plugins/session-store.d.ts +45 -0
  409. package/dist/plugins/session-store.js +51 -0
  410. package/dist/plugins/sql-driver.d.ts +77 -0
  411. package/dist/plugins/sql-driver.js +4 -0
  412. package/dist/plugins/sql-escape.d.ts +8 -0
  413. package/dist/plugins/sql-escape.js +8 -0
  414. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  415. package/dist/plugins/sql-row-helpers.js +25 -0
  416. package/dist/plugins/store-backend.d.ts +103 -0
  417. package/dist/plugins/store-backend.js +120 -27
  418. package/dist/plugins/store-contracts.d.ts +62 -0
  419. package/dist/plugins/store-contracts.js +3 -0
  420. package/dist/plugins/task-attachment-store.d.ts +60 -0
  421. package/dist/plugins/task-attachment-store.js +49 -3
  422. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  423. package/dist/plugins/task-list-store-sql.js +21 -0
  424. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  425. package/dist/plugins/tidb-approval-store.js +6 -0
  426. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  427. package/dist/plugins/tidb-breaker-state.js +5 -0
  428. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  429. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  430. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  431. package/dist/plugins/tidb-cost-quota.js +1 -0
  432. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  433. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  434. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  435. package/dist/plugins/tidb-image-bake.js +8 -0
  436. package/dist/plugins/tidb-image-index.d.ts +8 -0
  437. package/dist/plugins/tidb-image-index.js +8 -0
  438. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  439. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  440. package/dist/plugins/tidb-pool.d.ts +56 -0
  441. package/dist/plugins/tidb-pool.js +181 -1
  442. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  443. package/dist/plugins/tidb-rate-limiter.js +5 -2
  444. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  445. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  446. package/dist/plugins/tidb-run-store.d.ts +7 -0
  447. package/dist/plugins/tidb-run-store.js +7 -0
  448. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  449. package/dist/plugins/tidb-session-policy-store.js +5 -0
  450. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  451. package/dist/plugins/tidb-session-storage.js +62 -3
  452. package/dist/plugins/tidb-session-store.d.ts +142 -0
  453. package/dist/plugins/tidb-session-store.js +264 -16
  454. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  455. package/dist/plugins/tidb-tool-result-store.js +7 -0
  456. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  457. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  458. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  459. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  460. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  461. package/dist/plugins/tool-result-store-sql.js +38 -2
  462. package/dist/plugins/web-search.d.ts +35 -0
  463. package/dist/plugins/web-search.js +45 -9
  464. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  465. package/dist/plugins/workflow-journal-limits.js +10 -0
  466. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  467. package/dist/plugins/workflow-journal-store-sql.js +58 -2
  468. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  469. package/dist/plugins/workflow-run-store-sql.js +84 -6
  470. package/dist/plugins/worktree-isolation.d.ts +45 -0
  471. package/dist/plugins/worktree-isolation.js +105 -6
  472. package/dist/plugins/write-behind-counter.d.ts +56 -1
  473. package/dist/plugins/write-behind-counter.js +53 -14
  474. package/dist/principal-jwt.d.ts +24 -0
  475. package/dist/principal-jwt.js +60 -5
  476. package/dist/project-memory.d.ts +22 -0
  477. package/dist/project-memory.js +144 -15
  478. package/dist/prompts-domain-validate.d.ts +28 -0
  479. package/dist/prompts-domain-validate.js +42 -1
  480. package/dist/question.d.ts +33 -0
  481. package/dist/question.js +69 -2
  482. package/dist/resource-suspend.d.ts +21 -0
  483. package/dist/resource-suspend.js +16 -0
  484. package/dist/router/route-orchestration.d.ts +78 -0
  485. package/dist/router/route-orchestration.js +77 -0
  486. package/dist/run-local.d.ts +32 -0
  487. package/dist/run-local.js +172 -8
  488. package/dist/runs.d.ts +189 -4
  489. package/dist/runs.js +465 -24
  490. package/dist/runtime-caps-resolver.d.ts +106 -0
  491. package/dist/runtime-caps-resolver.js +89 -4
  492. package/dist/runtime-governance.d.ts +84 -0
  493. package/dist/runtime-governance.js +127 -1
  494. package/dist/sandbox-pkg-source.d.ts +23 -0
  495. package/dist/sandbox-pkg-source.js +37 -0
  496. package/dist/sealed-key.d.ts +54 -0
  497. package/dist/sealed-key.js +68 -3
  498. package/dist/security.d.ts +108 -0
  499. package/dist/security.js +145 -6
  500. package/dist/sema-registry.d.ts +21 -0
  501. package/dist/sema-registry.js +21 -0
  502. package/dist/session-leaf-bus.d.ts +31 -0
  503. package/dist/session-leaf-bus.js +32 -0
  504. package/dist/session-sync-content.d.ts +46 -0
  505. package/dist/session-sync-content.js +47 -1
  506. package/dist/session-sync-kernel.d.ts +89 -0
  507. package/dist/session-sync-kernel.js +48 -3
  508. package/dist/session-sync.d.ts +125 -0
  509. package/dist/session-sync.js +181 -8
  510. package/dist/session-titler.d.ts +26 -0
  511. package/dist/session-titler.js +41 -7
  512. package/dist/session-watch.d.ts +53 -1
  513. package/dist/session-watch.js +73 -9
  514. package/dist/sighup-idle.d.ts +30 -0
  515. package/dist/sighup-idle.js +12 -1
  516. package/dist/spec-fields.d.ts +91 -3
  517. package/dist/spec-fields.js +116 -4
  518. package/dist/task-cwd.d.ts +50 -0
  519. package/dist/task-cwd.js +68 -1
  520. package/dist/task-mcp.d.ts +47 -0
  521. package/dist/task-mcp.js +29 -0
  522. package/dist/task-settings.d.ts +175 -0
  523. package/dist/task-settings.js +226 -11
  524. package/dist/task-workflow.d.ts +67 -0
  525. package/dist/task-workflow.js +75 -8
  526. package/dist/tool-approval.d.ts +91 -0
  527. package/dist/tool-approval.js +215 -0
  528. package/dist/trace/artifacts.d.ts +9 -0
  529. package/dist/trace/artifacts.js +30 -5
  530. package/dist/trace/core-keyset-guard.d.ts +18 -0
  531. package/dist/trace/ledger-sink.d.ts +43 -0
  532. package/dist/trace/ledger-sink.js +64 -5
  533. package/dist/trace/project.d.ts +133 -0
  534. package/dist/trace/project.js +189 -2
  535. package/dist/trace/redact.d.ts +19 -0
  536. package/dist/trace/redact.js +66 -9
  537. package/dist/usage-analytics.d.ts +26 -0
  538. package/dist/usage-analytics.js +26 -3
  539. package/package.json +1 -1
@@ -1,7 +1,50 @@
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
+ */
1
31
  import { createHash } from "node:crypto";
2
32
  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. */
3
36
  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。 */
4
45
  export const SQL_BLOB_DEFAULT_MAX_BYTES = 6_291_456;
46
+ /** typed 判别错:blob 超过 SQL 店的承载帽。判别码供店侧结果映射/HTTP 面/日志使用——绝不能与
47
+ * transient 写失败混在一类(那类是可重试的 502;这类重试永远不会成功)。 */
5
48
  export class BlobTooLargeError extends Error {
6
49
  code = "blob_too_large_for_sql";
7
50
  constructor(byteLen, capBytes) {
@@ -12,7 +55,11 @@ export class BlobTooLargeError extends Error {
12
55
  this.name = "BlobTooLargeError";
13
56
  }
14
57
  }
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). */
15
61
  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). */
16
63
  async function mapBounded(items, limit, fn) {
17
64
  let i = 0;
18
65
  const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
@@ -23,15 +70,33 @@ async function mapBounded(items, limit, fn) {
23
70
  });
24
71
  await Promise.all(workers);
25
72
  }
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
+ */
26
85
  export class SqlBlobBackend {
27
86
  dialect;
28
87
  pool;
88
+ /** Effective per-blob cap: explicit ctor value wins; else tidb=SQL_BLOB_DEFAULT_MAX_BYTES, pg=uncapped. */
29
89
  capBytes;
30
90
  constructor(dialect, pool, maxBytes) {
31
91
  this.dialect = dialect;
32
92
  this.pool = pool;
33
93
  this.capBytes = maxBytes ?? (dialect === "tidb" ? SQL_BLOB_DEFAULT_MAX_BYTES : Number.POSITIVE_INFINITY);
34
94
  }
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. */
35
100
  async putBlob(hash, bytes) {
36
101
  if (bytes.byteLength > this.capBytes)
37
102
  throw new BlobTooLargeError(bytes.byteLength, this.capBytes);
@@ -42,6 +107,8 @@ export class SqlBlobBackend {
42
107
  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()]);
43
108
  }
44
109
  }
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). */
45
112
  async getBlob(hash) {
46
113
  if (this.dialect === "tidb") {
47
114
  const [rows] = await this.pool.query("SELECT bytes FROM snapshot_blob WHERE blob_hash = ?", [hash]);
@@ -50,14 +117,22 @@ export class SqlBlobBackend {
50
117
  const b = await this.pool.query("SELECT bytes FROM snapshot_blob WHERE blob_hash = $1", [hash]);
51
118
  return b.rows[0]?.bytes != null ? new Uint8Array(b.rows[0].bytes) : undefined;
52
119
  }
120
+ /** The subset of `hashes` that exist — a single `blob_hash IN (...)` SELECT (idempotency / reap probe). */
53
121
  async hasBlobs(hashes) {
54
122
  return sqlHasBlobs(this.dialect, this.pool, hashes);
55
123
  }
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). */
56
128
  async deleteBlobs(hashes) {
57
129
  const deleted = await sqlDeleteIndexRowsPastGrace(this.dialect, this.pool, hashes);
58
130
  return deleted.length;
59
131
  }
60
132
  }
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). */
61
136
  async function sqlHasBlobs(dialect, pool, hashes) {
62
137
  if (hashes.length === 0)
63
138
  return new Set();
@@ -71,10 +146,16 @@ async function sqlHasBlobs(dialect, pool, hashes) {
71
146
  const r = await pool.query(`SELECT blob_hash FROM snapshot_blob WHERE blob_hash IN (${ph})`, distinct);
72
147
  return new Set(r.rows.map((row) => row.blob_hash));
73
148
  }
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 整块字节。 */
74
152
  export async function sqlBlobSizes(dialect, pool, hashes) {
75
153
  if (hashes.length === 0)
76
154
  return new Map();
77
155
  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/块是保守值。
78
159
  const CHUNK = 1000;
79
160
  const out = new Map();
80
161
  for (let i = 0; i < distinct.length; i += CHUNK) {
@@ -94,6 +175,10 @@ export async function sqlBlobSizes(dialect, pool, hashes) {
94
175
  }
95
176
  return out;
96
177
  }
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. */
97
182
  async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
98
183
  if (hashes.length === 0)
99
184
  return [];
@@ -101,6 +186,8 @@ async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
101
186
  const cutoff = new Date(Date.now() - BLOB_GC_GRACE_MS);
102
187
  if (dialect === "tidb") {
103
188
  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).
104
191
  const grace = await pool
105
192
  .query(`SELECT blob_hash FROM snapshot_blob WHERE created_at < ? AND blob_hash IN (${ph})`, [cutoff, ...distinct])
106
193
  .then(([rows]) => rows.map((r) => String(r.blob_hash)))
@@ -119,10 +206,30 @@ async function sqlDeleteIndexRowsPastGrace(dialect, pool, hashes) {
119
206
  .catch(() => undefined);
120
207
  return (r?.rows ?? []).map((row) => row.blob_hash);
121
208
  }
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
+ */
122
225
  export class MinioBlobBackend {
123
226
  cfg;
124
227
  keyPrefix;
125
228
  ttl;
229
+ /** The SQL index pool+dialect, or undefined for an index-LESS backend (the migration script + the unit suite, which
230
+ * exercise ONLY the object I/O — put/get/has-by-HEAD/delete — with no DB). When undefined, hasBlobs falls back to a
231
+ * HEAD-per-hash probe and deleteBlobs ignores the grace window (the script is a stop-the-world copy, not the live GC
232
+ * path). The file-snapshot stores ALWAYS pass the pool, so the live reference-tracking always has the index. */
126
233
  indexPool;
127
234
  indexDialect;
128
235
  constructor(cfg, index) {
@@ -138,6 +245,8 @@ export class MinioBlobBackend {
138
245
  return `${this.keyPrefix}${hash}`;
139
246
  }
140
247
  presign(hash, method) {
248
+ // presignS3Url only declares GET|PUT in its param type, but the SigV4 canonicalization is identical for any verb
249
+ // (host-only SignedHeaders, UNSIGNED-PAYLOAD) — HEAD/DELETE sign correctly with the same string math. Cast the verb.
141
250
  return presignS3Url({
142
251
  endpoint: this.cfg.endpoint,
143
252
  bucket: this.cfg.bucket,
@@ -149,6 +258,14 @@ export class MinioBlobBackend {
149
258
  expiresSec: this.ttl,
150
259
  });
151
260
  }
261
+ /** PUT the bytes under the content-addressed key, THEN upsert the SQL INDEX row (bytes NULL, created_at refresh on
262
+ * conflict — the SAME grace discipline as SqlBlobBackend, so a just-PUT blob an in-flight snapshot needs is touched=
263
+ * now and protected). Idempotent (same hash ⇒ overwrite-with-identical-bytes + created_at bump). A non-2xx PUT throws
264
+ * (an upload failure must surface — a silent drop would later read_failed on restore). The PUT happens BEFORE the
265
+ * index upsert so an index row never claims a blob the object store doesn't hold (an index row with no object would
266
+ * let hasBlobs report a phantom present; the reverse — an object with no index — is harmless and pruned by a bucket
267
+ * lifecycle if it ever happens). NOTE no post-PUT read-back verify on the LIVE path (perf): content-addressing
268
+ * catches a corrupted PUT at the next getBlob sha-check (fail-closed); the one-time migration script DOES read-back. */
152
269
  async putBlob(hash, bytes) {
153
270
  const url = this.presign(hash, "PUT");
154
271
  const res = await fetch(url, { method: "PUT", body: bytes });
@@ -156,6 +273,9 @@ export class MinioBlobBackend {
156
273
  const detail = await res.text().catch(() => "");
157
274
  throw new Error(`MinioBlobBackend.putBlob ${hash}: HTTP ${res.status} ${detail.slice(0, 200)}`);
158
275
  }
276
+ // Upsert the bytes-NULL INDEX row (byte_len + created_at) so the orphan/grace reference-tracking + presence checks
277
+ // see this blob. Same created_at-refresh-on-conflict as the SQL backend. (No index pool ⇒ object-only mode = the
278
+ // migration/unit path; the live stores always pass it.)
159
279
  if (this.indexPool && this.indexDialect) {
160
280
  if (this.indexDialect === "tidb") {
161
281
  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()]);
@@ -165,6 +285,12 @@ export class MinioBlobBackend {
165
285
  }
166
286
  }
167
287
  }
288
+ /** GET the bytes for `hash`, then VERIFY content-address (sha256(bytes)===hash). THREE branches (finding 11):
289
+ * - 404 → undefined (genuinely ABSENT — the file-snapshot store treats undefined as a missing blob).
290
+ * - sha256 MISMATCH → undefined (corruption/poisoning; treat corrupt-as-absent, fail-CLOSED, NEVER wrong bytes).
291
+ * - a NON-2xx-NON-404 (5xx/throttle) OR a network/fetch REJECT → THROW (a TRANSIENT fault must fail LOUD so the
292
+ * restore/import surfaces an error, NOT silently drop a file as "missing"; the SQL backend likewise propagates a
293
+ * pool error). The snapshot store's restore() wraps applyManifest so this throw becomes a {ok:false} restore_failed. */
168
294
  async getBlob(hash) {
169
295
  const url = this.presign(hash, "GET");
170
296
  let res;
@@ -175,19 +301,24 @@ export class MinioBlobBackend {
175
301
  throw new Error(`MinioBlobBackend.getBlob ${hash}: transient fetch failure: ${err.message}`);
176
302
  }
177
303
  if (res.status === 404)
178
- return undefined;
304
+ return undefined; // genuinely absent
179
305
  if (!res.ok) {
180
306
  const detail = await res.text().catch(() => "");
181
307
  throw new Error(`MinioBlobBackend.getBlob ${hash}: transient HTTP ${res.status} ${detail.slice(0, 200)}`);
182
308
  }
183
309
  const buf = new Uint8Array(await res.arrayBuffer());
310
+ // content-address integrity: the bytes MUST hash to the key. A mismatch = corrupt/poisoned → undefined (absent), NOT wrong bytes.
184
311
  if (createHash("sha256").update(buf).digest("hex") !== hash)
185
312
  return undefined;
186
313
  return buf;
187
314
  }
315
+ /** The subset of `hashes` present — reads the SQL INDEX (cheaper than a HEAD-per-hash AND consistent with the SQL
316
+ * backend). Falls back to a presigned HEAD per hash ONLY in object-only mode (no index pool: the migration / unit
317
+ * path). The caller passes DISTINCT hashes (migration idempotency batch / reap orphan set / §10 presence pre-check). */
188
318
  async hasBlobs(hashes) {
189
319
  if (this.indexPool && this.indexDialect)
190
320
  return sqlHasBlobs(this.indexDialect, this.indexPool, hashes);
321
+ // object-only mode (migration/unit): HEAD probe, bounded concurrency.
191
322
  const distinct = [...new Set(hashes)];
192
323
  const present = new Set();
193
324
  await mapBounded(distinct, MINIO_OP_CONCURRENCY, async (hash) => {
@@ -197,13 +328,22 @@ export class MinioBlobBackend {
197
328
  present.add(hash);
198
329
  }
199
330
  catch {
331
+ /* treat an unreachable HEAD as absent — a false-absent only re-PUTs (idempotent) or re-checks; never a false-present */
200
332
  }
201
333
  });
202
334
  return present;
203
335
  }
336
+ /** D-1 孤儿对象 GC(clay 拍 2026-07-28):列举本后端 keyPrefix 下的全部对象(ListObjectsV2 分页,
337
+ * 零依赖 XML 取 <Key>/<LastModified>/<NextContinuationToken>)。**server 端 sweep 专用**——presign 的
338
+ * bucket-GET 列举能力绝不外发。响应非 2xx 即 throw(sweep 调用方自 catch:列举失败=本轮跳过,
339
+ * 绝不把「列不出来」当「没有孤儿」)。 */
204
340
  async *listObjects() {
205
341
  const f = this.cfg.fetchImpl ?? fetch;
206
342
  let token;
343
+ // 🔴 复审 F3:分页上限。异常/代理后端回环或恒定的 continuation-token 会让本循环无界翻页
344
+ // (实测可翻 5001 页仍不停),叠加维护 tick 就是逐轮堆积。超限 fail-loud —— 与本函数既有的
345
+ // 「列不出来 ≠ 没有孤儿」口径一致(宁可整轮失败也不给出残缺的「孤儿全集」)。
346
+ // 1000 页 × 1000 键/页 = 100 万对象,远超任何真实附件语料;真撞上=后端异常而非规模问题。
207
347
  const MAX_PAGES = 1000;
208
348
  let pages = 0;
209
349
  do {
@@ -224,14 +364,15 @@ export class MinioBlobBackend {
224
364
  if (!res.ok)
225
365
  throw new Error(`listObjects: MinIO ${res.status}`);
226
366
  const xml = await res.text();
367
+ // <Contents><Key>k</Key>…<LastModified>ISO</LastModified>…</Contents> —— 逐块取,键序无假设。
227
368
  for (const m of xml.matchAll(/<Contents>([\s\S]*?)<\/Contents>/g)) {
228
369
  const block = m[1];
229
370
  const key = /<Key>([^<]*)<\/Key>/.exec(block)?.[1];
230
371
  const lm = /<LastModified>([^<]*)<\/LastModified>/.exec(block)?.[1];
231
372
  if (key === undefined || !key.startsWith(this.keyPrefix))
232
- continue;
373
+ continue; // prefix 外键(理论不可达)跳过
233
374
  const ms = lm ? Date.parse(lm) : NaN;
234
- yield { hash: key.slice(this.keyPrefix.length), lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
375
+ yield { hash: key.slice(this.keyPrefix.length), lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() }; // 坏时间戳=按「新」处理(grace 保护,宁漏勿误删)
235
376
  }
236
377
  const truncated = /<IsTruncated>true<\/IsTruncated>/.test(xml);
237
378
  token = truncated ? /<NextContinuationToken>([^<]*)<\/NextContinuationToken>/.exec(xml)?.[1] : undefined;
@@ -239,6 +380,8 @@ export class MinioBlobBackend {
239
380
  throw new Error("listObjects: truncated response without NextContinuationToken");
240
381
  } while (token !== undefined);
241
382
  }
383
+ /** 单对象 HEAD 探针(复审 B-F1 的删前复查面):返回当前 LastModified(缺席=对象已不在)。
384
+ * put 会 PUT 同键 ⇒ 刷新 LastModified,所以「删前再 HEAD 一次」能看见「刚刚被重新引用」的对象。 */
242
385
  async headObject(hash) {
243
386
  const f = this.cfg.fetchImpl ?? fetch;
244
387
  const res = await f(this.presign(hash, "HEAD"), { method: "HEAD" });
@@ -246,17 +389,28 @@ export class MinioBlobBackend {
246
389
  return undefined;
247
390
  const lm = res.headers.get("last-modified");
248
391
  const ms = lm ? Date.parse(lm) : NaN;
249
- return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() };
392
+ return { lastModifiedMs: Number.isFinite(ms) ? ms : Date.now() }; // 无/坏时间戳=按「新」处理(宁漏勿误删)
250
393
  }
394
+ /** Byte-delete the given hashes — called ONLY from the async sweepOrphanBlobs reaper (NEVER the synchronous purge
395
+ * path). Honors the SAME created_at grace window as the SQL backend: with an index, DELETE the grace-passed index
396
+ * rows first (the index's created_at is the authoritative age — an S3 object carries no service-controlled timestamp
397
+ * the backend reads), then delete the MinIO objects ONLY for those grace-passed hashes (so a just-PUT blob an
398
+ * in-flight snapshot needs is NOT delete-in-use). ERROR-TOLERANT + BOUNDED: each object delete is independent (a
399
+ * failure is skipped → retried next sweep), at most MINIO_OP_CONCURRENCY in flight (no FD/throttle storm). In
400
+ * object-only mode (no index pool: the migration/unit cleanup path) it deletes every given hash with no grace. */
251
401
  async deleteBlobs(hashes) {
252
402
  if (hashes.length === 0)
253
403
  return 0;
404
+ // Determine which hashes are grace-passed AND remove their index rows (so the index no longer claims them present).
405
+ // Order: index-row DELETE FIRST, then the object DELETE — if the object delete fails, the orphan re-appears only as
406
+ // an object with no index, which a later sweep can't re-target; acceptable (under-delete is harmless, and a bucket
407
+ // lifecycle rule prunes index-less objects). Doing it index-first keeps hasBlobs honest immediately.
254
408
  let toDelete;
255
409
  if (this.indexPool && this.indexDialect) {
256
410
  toDelete = await sqlDeleteIndexRowsPastGrace(this.indexDialect, this.indexPool, hashes);
257
411
  }
258
412
  else {
259
- toDelete = [...new Set(hashes)];
413
+ toDelete = [...new Set(hashes)]; // object-only mode (migration/unit cleanup): no grace, delete all given
260
414
  }
261
415
  if (toDelete.length === 0)
262
416
  return 0;
@@ -264,13 +418,16 @@ export class MinioBlobBackend {
264
418
  await mapBounded(toDelete, MINIO_OP_CONCURRENCY, async (hash) => {
265
419
  try {
266
420
  const res = await (this.cfg.fetchImpl ?? fetch)(this.presign(hash, "DELETE"), { method: "DELETE" });
421
+ // 🔴 复审 F4:统计**真实**删除数——index-less(附件)分支此前恒返回入参长度,MinIO 完全不可写
422
+ // 时指标/日志照报「清理成功」。2xx/404(已不在)都算删成功;其余=失败,不计数。
267
423
  if (res.ok || res.status === 404)
268
424
  okCount++;
269
425
  }
270
426
  catch {
427
+ /* error-tolerant: a failed/timed-out object delete is skipped → retried next sweep (eventual consistency) */
271
428
  }
272
429
  });
273
- return okCount;
430
+ return okCount; // the index rows we removed = the work done (best-effort object deletes follow async)
274
431
  }
275
432
  }
276
433
  //# sourceMappingURL=blob-backend.js.map
@@ -1,28 +1,92 @@
1
+ /**
2
+ * Cross-replica circuit-breaker state (core 1.38 `BreakerState`) — SINGLE-FILE DUAL-DIALECT
3
+ * (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical `TiDBBreakerState` /
4
+ * `PgBreakerState` class names survive as thin ctor subclasses (src/plugins/tidb-breaker-state.ts +
5
+ * src/plugins/pg-breaker-state.ts are now re-export shims) so every consumer (store-backend.ts, the
6
+ * fake-pool unit suites, the real dual-DB integration suite) is untouched.
7
+ *
8
+ * core's `createCircuitBreakerBrain` defaults to a per-PROCESS Map → on a multi-replica deployment
9
+ * every replica re-discovers a provider outage independently (slow fleet-wide reaction). This backs
10
+ * the breaker with the shared `circuit_breaker` table so a trip on one replica is learned by the others.
11
+ *
12
+ * IMPORTANT — `BreakerState.get/set` are **synchronous** (core calls them on the hot path of every
13
+ * brain invocation; a DB round-trip per call is untenable). So this is a **write-through cache**:
14
+ * - `get` / `set` operate on an in-process Map synchronously (so the local breaker is always correct);
15
+ * - `set` ALSO fires an async write-through to the shared table (best-effort; a DB hiccup must never
16
+ * break the request path — breaker state is advisory);
17
+ * - a periodic `refresh()` pulls the shared table back into the cache so this replica learns peers'
18
+ * trips. Consistency is therefore **eventual** (a replica may send a few extra requests before it
19
+ * learns a peer already opened the breaker — self-healing, never wrong in a dangerous direction).
20
+ *
21
+ * Strict per-call linearizable state would need an async `BreakerState` from core (a separate effort);
22
+ * for a consultative breaker, eventually-consistent write-through is the right trade (see core design/20).
23
+ *
24
+ * ── Dialect deltas, kept EXPLICIT (the ONLY place the two engines actually differ) ─────────────────
25
+ * - placeholders `?` vs `$n`
26
+ * - upsert form: `ON DUPLICATE KEY UPDATE … VALUES(col)` vs `ON CONFLICT (breaker_key) DO UPDATE SET
27
+ * … EXCLUDED.col`
28
+ * - schema ownership: TiDB DDL lives centrally in tidb-pool.ts; PG DDL is self-contained here
29
+ * (`PG_BREAKER_STATE_SCHEMA` / `ensureSchema`, re-exported off pg-breaker-state.ts) — disjoint table,
30
+ * so a standalone idempotent apply is safe (no central-schema shadowing), mirroring every other PG store.
31
+ * Everything else (write-through logic, the pending-guard race fix, refresh/prune, row→snapshot mapping,
32
+ * opened_at Date handling) is IDENTICAL — the SqlDriver seam (result unwrapping) is what let the row
33
+ * mapping collapse to one body; both drivers hand back the same `SqlRow` shape.
34
+ *
35
+ * Kept in lock-step with the real engines by test/pg-breaker-state-integration.test.ts (runs the SAME
36
+ * scenarios on BOTH real engines) and the cross-instance concurrency pin in
37
+ * test/file-stores-cross-instance-concurrency.test.ts (last-writer-wins overwrite semantics — a design
38
+ * question, not a defect; see that file's comment for the full reasoning).
39
+ */
1
40
  import type { Pool as MySqlPool } from "mysql2/promise";
2
41
  import type { Pool as PgPool, PoolClient } from "pg";
3
42
  import type { BreakerSnapshot, BreakerState } from "@sema-agent/core";
4
43
  import { type SqlDriver } from "./sql-driver.js";
44
+ /** PG translation of the tidb-pool.ts `circuit_breaker` DDL (DATETIME(3)→TIMESTAMPTZ(3)). Disjoint from other
45
+ * stores' tables, so a self-contained ensureSchema is safe (no central-schema shadowing). TiDB's DDL lives
46
+ * centrally in tidb-pool.ts (no PG-side twin needed there — production wires ONE ensureTidbSchema). */
5
47
  export declare const PG_BREAKER_STATE_SCHEMA: string[];
48
+ /** Idempotent schema apply for the breaker table (the PG twin of tidb-pool.ts ensureSchema, scoped to this store). */
6
49
  export declare function ensureSchema(pool: PgPool | PoolClient): Promise<void>;
50
+ /** Dual-dialect cross-replica BreakerState. See the file header for the dialect-delta ledger. */
7
51
  export declare class SqlBreakerState implements BreakerState {
8
52
  protected readonly db: SqlDriver;
53
+ /** LOW (SILENT-FALLBACK P1): write-through failure tap — streak per call, 0 on the recovery edge. */
9
54
  private readonly onWriteFail?;
10
55
  private cache;
11
56
  private timer?;
57
+ /** Keys with an in-flight write-through upsert. A concurrent `refresh()` must NOT overwrite or prune
58
+ * these — their newer local value may not be in the DB yet. Closes the set()-vs-refresh race (core
59
+ * 1.40.0 council finding #5). */
12
60
  private readonly pending;
13
61
  private writeFailStreak;
14
- constructor(db: SqlDriver, onWriteFail?: ((streak: number) => void) | undefined);
62
+ constructor(db: SqlDriver,
63
+ /** LOW (SILENT-FALLBACK P1): write-through failure tap — streak per call, 0 on the recovery edge. */
64
+ onWriteFail?: ((streak: number) => void) | undefined);
65
+ /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
15
66
  private q;
16
67
  get(key: string): BreakerSnapshot | undefined;
17
68
  set(key: string, snap: BreakerSnapshot): void;
18
69
  private writeThrough;
70
+ /**
71
+ * Pull the shared table into the local cache so this replica learns peers' trips, and PRUNE keys that
72
+ * vanished from the DB — but never touch a locally-pending write (its upsert may not have landed yet),
73
+ * else a just-`set` trip would be clobbered/reverted before it reaches the DB. Eventually consistent:
74
+ * a replica is at most one refresh interval behind a peer, never wrong in a data-losing way.
75
+ * `halfOpenInFlight` is intentionally not shared (it is a per-replica probe counter).
76
+ *
77
+ * SELECT text is byte-identical on both dialects (no placeholders) — written once, not through `q()`
78
+ * (same precedent as image-bake-store-sql.ts's queued-bake lookup).
79
+ */
19
80
  refresh(): Promise<void>;
81
+ /** Start the periodic refresh loop. `unref` so it never holds the process open. Returns `this`. */
20
82
  startRefresh(intervalMs?: number): this;
21
83
  stop(): void;
22
84
  }
85
+ /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
23
86
  export declare class TiDBBreakerState extends SqlBreakerState {
24
87
  constructor(pool: MySqlPool, onWriteFail?: (streak: number) => void);
25
88
  }
89
+ /** PostgreSQL binding — historical class name + ctor shape preserved. */
26
90
  export declare class PgBreakerState extends SqlBreakerState {
27
91
  constructor(pool: PgPool, onWriteFail?: (streak: number) => void);
28
92
  }
@@ -1,4 +1,7 @@
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). */
2
5
  export const PG_BREAKER_STATE_SCHEMA = [
3
6
  `CREATE TABLE IF NOT EXISTS circuit_breaker (
4
7
  breaker_key VARCHAR(190) NOT NULL,
@@ -9,21 +12,29 @@ export const PG_BREAKER_STATE_SCHEMA = [
9
12
  PRIMARY KEY (breaker_key)
10
13
  )`,
11
14
  ];
15
+ /** Idempotent schema apply for the breaker table (the PG twin of tidb-pool.ts ensureSchema, scoped to this store). */
12
16
  export async function ensureSchema(pool) {
13
17
  for (const stmt of PG_BREAKER_STATE_SCHEMA)
14
18
  await pool.query(stmt);
15
19
  }
20
+ /** Dual-dialect cross-replica BreakerState. See the file header for the dialect-delta ledger. */
16
21
  export class SqlBreakerState {
17
22
  db;
18
23
  onWriteFail;
19
24
  cache = new Map();
20
25
  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). */
21
29
  pending = new Set();
22
30
  writeFailStreak = 0;
23
- constructor(db, onWriteFail) {
31
+ constructor(db,
32
+ /** LOW (SILENT-FALLBACK P1): write-through failure tap — streak per call, 0 on the recovery edge. */
33
+ onWriteFail) {
24
34
  this.db = db;
25
35
  this.onWriteFail = onWriteFail;
26
36
  }
37
+ /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
27
38
  q(tidb, pg) {
28
39
  return this.db.dialect === "tidb" ? tidb : pg;
29
40
  }
@@ -31,9 +42,9 @@ export class SqlBreakerState {
31
42
  return this.cache.get(key);
32
43
  }
33
44
  set(key, snap) {
34
- this.cache.set(key, snap);
35
- this.pending.add(key);
36
- void this.writeThrough(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
37
48
  }
38
49
  async writeThrough(key, snap) {
39
50
  try {
@@ -46,18 +57,41 @@ export class SqlBreakerState {
46
57
  try {
47
58
  this.onWriteFail?.(0);
48
59
  }
49
- catch { }
50
- }
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.
51
67
  }
52
68
  catch {
53
69
  try {
54
70
  this.onWriteFail?.(++this.writeFailStreak);
55
71
  }
56
- catch { }
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.
57
77
  }
58
78
  }
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
+ */
59
89
  async refresh() {
60
90
  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.
61
95
  const pendingAtStart = new Set(this.pending);
62
96
  const guarded = (key) => this.pending.has(key) || pendingAtStart.has(key);
63
97
  const { rows } = await this.db.query("SELECT breaker_key, phase, failures, opened_at FROM circuit_breaker");
@@ -66,7 +100,7 @@ export class SqlBreakerState {
66
100
  const key = String(r.breaker_key);
67
101
  seen.add(key);
68
102
  if (guarded(key))
69
- continue;
103
+ continue; // don't clobber a locally-pending newer value (#5)
70
104
  this.cache.set(key, {
71
105
  phase: r.phase,
72
106
  failures: Number(r.failures),
@@ -78,8 +112,10 @@ export class SqlBreakerState {
78
112
  this.cache.delete(key);
79
113
  }
80
114
  catch {
115
+ // Keep the last-known cache on a transient read failure (fail static, don't blank the breaker).
81
116
  }
82
117
  }
118
+ /** Start the periodic refresh loop. `unref` so it never holds the process open. Returns `this`. */
83
119
  startRefresh(intervalMs = 5_000) {
84
120
  this.timer ??= setInterval(() => void this.refresh(), intervalMs);
85
121
  this.timer.unref?.();
@@ -91,11 +127,13 @@ export class SqlBreakerState {
91
127
  this.timer = undefined;
92
128
  }
93
129
  }
130
+ /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
94
131
  export class TiDBBreakerState extends SqlBreakerState {
95
132
  constructor(pool, onWriteFail) {
96
133
  super(mysqlDriver(pool), onWriteFail);
97
134
  }
98
135
  }
136
+ /** PostgreSQL binding — historical class name + ctor shape preserved. */
99
137
  export class PgBreakerState extends SqlBreakerState {
100
138
  constructor(pool, onWriteFail) {
101
139
  super(pgDriver(pool), onWriteFail);