@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,9 +1,52 @@
1
+ /**
2
+ * Sandbox-image-pool BAKE store — SINGLE-FILE DUAL-DIALECT (design/158 A12 定型半场; the 4th collapse
3
+ * after background-agent / mailbox / task-list / roster). ONE implementation, TWO dialects; the
4
+ * historical `TiDBImageBake` / `PgImageBake` class names survive as thin ctor subclasses so every
5
+ * consumer (store-backend.ts, pg-pool.ts, the unit + real-DB suites) is untouched.
6
+ *
7
+ * A bake is the async, resumable, auto-registering execution of `build.sh` on the privileged build-host
8
+ * bake-runner. image-api (this, the unprivileged controller) owns the durable job row + the per-bake event
9
+ * log + the single-flight build lease; the runner CLAIMS a queued bake, runs build.sh, and POSTs each
10
+ * build.sh JSON line back to the ingest endpoint, which appends to the event log the SSE reader serves
11
+ * (run-trace mechanics verbatim: `(bakeId,seq)` PK, ++seq, getEvents(after), maxSeq, retainedFrom→416,
12
+ * heartbeat-not-persisted).
13
+ *
14
+ * This is a DEDICATED table set — NOT a reuse of task_run/task_event (FK-coupled to task_active + the run
15
+ * reaper + the /v1/runs list; co-mingling bakes — no session, no spend, no checkpoint — would leak non-task
16
+ * rows into reapStale/listRuns, a category error). The shape mirrors TiDBRunStore: durable idempotency via a
17
+ * unique-key CAS (like createRun), a conditional-UPDATE single-flight claim (like requestCancel), and a
18
+ * reaper twin.
19
+ *
20
+ * The split that makes the SSE reader correct (P2.3 MUST-FIX): the COARSE 4-value `status`
21
+ * (queued|running|done|failed) is what the reader polls for terminality; build.sh's 8-value `state`
22
+ * (PENDING→…→COMPLETE|FAILED|CANCELLED) is denormalized for the UI progress bar ONLY and never drives stream
23
+ * termination (a non-terminal PUSHING/VERIFYING state must NOT truncate the stream before `done`).
24
+ *
25
+ * ── Dialect deltas, kept EXPLICIT (never hidden behind an abstraction) ────────────────────────────────
26
+ * - `?` placeholders vs `$n` - `CAST(? AS JSON)` vs `$n::jsonb` (argv equality)
27
+ * - `INSERT IGNORE` vs `ON CONFLICT DO NOTHING` - affectedRows vs rowCount (via SqlDriver)
28
+ * - dup-key `ER_DUP_ENTRY` vs SQLSTATE `23505` - which UNIQUE collided: mysql2 `sqlMessage` text
29
+ * vs pg `err.constraint` (uq_line naming differs)
30
+ * - `BEGIN PESSIMISTIC` (TiDB current read) vs `BEGIN` (PG READ COMMITTED under the sentinel lock)
31
+ * - the ADMISSION statement itself differs (see createBakeIfIdle) — both are correct under the
32
+ * sentinel lock, and both are preserved verbatim because the real-DB suites are the oracle
33
+ * - PG-ONLY protocol-byte defenses: `pgSafeJsonStringify` on event payloads, and the setTerminal
34
+ * identity-fold (`invalidIdentity`) + `pgSanitizeText` on error/errorCode/tag. TiDB stores verbatim.
35
+ * - schema ownership: the TiDB DDL lives in tidb-pool.ts SCHEMA_STATEMENTS; the PG DDL is exported
36
+ * from HERE (PG_IMAGE_BAKE_SCHEMA + ensurePgImageBakeSchema) for pg-pool.ts + the integration test.
37
+ */
1
38
  import { uuidv7 } from "@sema-agent/core";
2
39
  import { randomBytes } from "node:crypto";
3
40
  import { pgSanitizeText, pgSafeJsonStringify, pgHasUnstorable } from "./pg-safe-json.js";
4
41
  import { parseJsonOr as parseJson, toIso as iso } from "./sql-row-helpers.js";
5
42
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
6
43
  import { mapBakeRow as mapRow, BAKE_SELECT_COLS as SELECT_COLS, } from "./store-contracts.js";
44
+ /** PG translation of the image_bake / image_bake_event / image_bake_lease DDL in tidb-pool.ts SCHEMA_STATEMENTS
45
+ * (JSON→JSONB, TINYINT→SMALLINT, DATETIME(3)→TIMESTAMPTZ(3), inline UNIQUE/KEY→named CONSTRAINT + CREATE INDEX).
46
+ * TINYINT(1) 归一(clay 批 2026-07-26):布尔列的 PG 映射统一 SMALLINT 0/1(此前本家族用 BOOLEAN)——
47
+ * 两方言驱动读回同为 number ⇒ 不再存在「忘了配的适配器」这个病族;写入参与 SQL 字面量一律 0/1,
48
+ * 读侧 `!!`/`Boolean()` 保持,store 出口 JS 类型逐字节不变(真双库套件的严格值断言为证)。
49
+ * The uq_line UNIQUE is given an explicit name so a 23505 can be attributed to it via err.constraint. */
7
50
  export const PG_IMAGE_BAKE_SCHEMA = [
8
51
  `CREATE TABLE IF NOT EXISTS image_bake (
9
52
  bake_id VARCHAR(64) NOT NULL,
@@ -56,20 +99,33 @@ export const PG_IMAGE_BAKE_SCHEMA = [
56
99
  expires_at TIMESTAMPTZ(3) NOT NULL,
57
100
  PRIMARY KEY (pool)
58
101
  )`,
102
+ // Admission sentinel: ONE always-present row per builder pool, `SELECT … FOR UPDATE`-locked to SERIALIZE the
103
+ // createBakeIfIdle busy-check+insert (so the M4 single-flight admission is genuinely atomic, not a snapshot
104
+ // NOT EXISTS that two concurrent READ COMMITTED txns both pass — see createBakeIfIdle). Dialect-uniform with
105
+ // the TiDB twin so a future divergence FAILS LOUD in the shared real-DB suite.
59
106
  `CREATE TABLE IF NOT EXISTS image_bake_admit (
60
107
  pool VARCHAR(32) NOT NULL,
61
108
  PRIMARY KEY (pool)
62
109
  )`,
63
110
  ];
111
+ /** Idempotent self-contained PG schema apply (for the integration test; central aggregation is done separately). */
64
112
  export async function ensurePgImageBakeSchema(pool, poolName = "standard") {
65
113
  for (const stmt of PG_IMAGE_BAKE_SCHEMA)
66
114
  await pool.query(stmt);
115
+ // Seed the always-present admission sentinel (idempotent) so createBakeIfIdle can FOR UPDATE-lock it.
67
116
  await pool.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [poolName]);
68
117
  }
118
+ /** Dup-key detection — mysql2 names it `ER_DUP_ENTRY`, PG raises SQLSTATE 23505. */
69
119
  function isDupKey(dialect, err) {
70
120
  const code = err?.code;
71
121
  return Boolean(err) && code === (dialect === "tidb" ? "ER_DUP_ENTRY" : "23505");
72
122
  }
123
+ /** Which UNIQUE key did the dup collide on? The ATTRIBUTION CHANNEL differs by engine: mysql2 names the key in
124
+ * `sqlMessage` ("Duplicate entry '…' for key 'image_bake_event.uq_line'" vs "…'image_bake_event.PRIMARY'");
125
+ * PG names the violated constraint in `err.constraint` (the PK is auto-named `image_bake_event_pkey`, the line
126
+ * UNIQUE is explicitly named `uq_image_bake_event_line` so it is attributable at all).
127
+ * M2: a line-unique dup is a BENIGN at-least-once re-send (already stored → no-op); a `PRIMARY` (bake_id,seq)
128
+ * dup is a same-seq RACE the caller must RETRY (a fresh maxSeq+1), never swallow. */
73
129
  function dupKeyName(dialect, err) {
74
130
  if (dialect === "tidb") {
75
131
  const msg = String(err?.sqlMessage ?? err?.message ?? "");
@@ -88,16 +144,21 @@ function dupKeyName(dialect, err) {
88
144
  }
89
145
  const INSERT_BAKE_COLS = "INSERT INTO image_bake (bake_id, profile, bands, base_ref, push, dry_run, logs, argv, status, " +
90
146
  "idem_key, cancel_requested, requested_by, created_at, updated_at) ";
147
+ /** Dual-dialect BAKE store. See the file header for the dialect-delta ledger. */
91
148
  export class SqlImageBake {
92
149
  db;
93
150
  poolName;
94
- constructor(db, poolName = "standard") {
151
+ constructor(db,
152
+ /** The builder pool a lease serializes on (one docker daemon ⇒ one pool today). */
153
+ poolName = "standard") {
95
154
  this.db = db;
96
155
  this.poolName = poolName;
97
156
  }
157
+ /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
98
158
  q(tidb, pg) {
99
159
  return this.db.dialect === "tidb" ? tidb : pg;
100
160
  }
161
+ /** The 12 bound values shared by both admission INSERTs (column order === INSERT_BAKE_COLS). */
101
162
  insertParams(bakeId, input, now) {
102
163
  return [
103
164
  bakeId,
@@ -114,6 +175,12 @@ export class SqlImageBake {
114
175
  now,
115
176
  ];
116
177
  }
178
+ /**
179
+ * Create a bake, queued. Durable submit-idempotency mirrors createRun's CAS: `idem_key` is UNIQUE, so a
180
+ * re-submit with the same key races to INSERT and the loser catches the dup-key, then re-reads + returns the
181
+ * EXISTING row (the live bake) instead of a second job. A null `idemKey` always inserts (no dedupe requested).
182
+ * Returns `{ created, bake }` so the handler can answer 202 either way (idempotent dup ⇒ created:false).
183
+ */
117
184
  async createBake(input) {
118
185
  const bakeId = uuidv7();
119
186
  const now = new Date();
@@ -133,6 +200,35 @@ export class SqlImageBake {
133
200
  throw new Error("createBake: row vanished after insert");
134
201
  return { created: true, bake };
135
202
  }
203
+ /**
204
+ * M4 (§P2.1/§P2.7 — the 409-busy MUST): create a queued bake ATOMICALLY only when no other non-dryRun bake is
205
+ * in flight, so two concurrent keyless POSTs can NEVER both queue (the check-then-act in the submit handler —
206
+ * findActiveAny() then createBake() as two awaited round-trips — interleaves: both see null-busy, both insert).
207
+ * `uq_idem` only dedupes non-null idem_keys (a keyless submit is NULL = distinct), and the build lease is taken
208
+ * at CLAIM not submit, so neither backstops this — the admission itself must be serialized.
209
+ *
210
+ * A bare conditional INSERT (`INSERT … SELECT … WHERE NOT EXISTS(...)`) is NOT sufficient ON ITS OWN: under
211
+ * TiDB's default REPEATABLE READ (and PG's READ COMMITTED) two concurrent autocommit conditional inserts each
212
+ * evaluate NOT EXISTS against a snapshot that excludes the other's still-uncommitted row (PG takes no
213
+ * predicate/gap lock), so BOTH can pass and double-admit. The only backstop today is the build lease taken at
214
+ * CLAIM, so a double-admit degrades the 409-busy/202-attach contract rather than double-building — but the
215
+ * contract is the point.
216
+ *
217
+ * Fix: serialize the busy-check+insert on the per-pool admission sentinel (`image_bake_admit`) with
218
+ * `SELECT … FOR UPDATE` inside one transaction. The second admitter blocks on the sentinel row lock until the
219
+ * first commits, then sees the committed in-flight bake and inserts 0 → `created:false`, and the caller
220
+ * re-reads the active bake (findActiveByArgv for an idempotent attach, else findActiveAny) for the
221
+ * 202-attach / 409 body. A dryRun bypasses this path entirely (never takes the lease — parallel-safe) and uses
222
+ * plain createBake.
223
+ *
224
+ * 🔴 DIALECT DIVERGENCE (deliberate, both preserved verbatim — the real-DB suites are the oracle):
225
+ * - TiDB: `BEGIN PESSIMISTIC` + a separate `SELECT 1 … FOR UPDATE` busy-check, then a plain INSERT. The
226
+ * pessimistic txn is REQUIRED: TiDB's snapshot (REPEATABLE READ) txn would evaluate a NOT EXISTS against
227
+ * the txn-start snapshot and miss a rival's just-committed row EVEN under the sentinel lock.
228
+ * - PG: plain `BEGIN` + one `INSERT … SELECT … WHERE NOT EXISTS(...)`. READ COMMITTED takes a FRESH
229
+ * statement snapshot, so the NOT EXISTS run after the sentinel lock is granted already sees the committed
230
+ * rival — the conditional insert is atomic enough once serialized.
231
+ */
136
232
  async createBakeIfIdle(input) {
137
233
  const bakeId = uuidv7();
138
234
  const now = new Date();
@@ -142,8 +238,12 @@ export class SqlImageBake {
142
238
  try {
143
239
  if (this.db.dialect === "tidb") {
144
240
  await conn.beginPessimistic();
241
+ // Lock the always-present per-pool admission sentinel so concurrent admissions serialize (defensive INSERT
242
+ // IGNORE first in case ensureSchema seeded a different poolName).
145
243
  await conn.query("INSERT IGNORE INTO image_bake_admit (pool) VALUES (?)", [this.poolName]);
146
244
  await conn.query("SELECT pool FROM image_bake_admit WHERE pool = ? FOR UPDATE", [this.poolName]);
245
+ // Busy-check as a CURRENT read (FOR UPDATE) — under the sentinel lock this is a single serialized admitter,
246
+ // so it sees the prior admitter's committed in-flight bake (no stale-snapshot phantom).
147
247
  const busy = await conn.query("SELECT 1 FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 LIMIT 1 FOR UPDATE");
148
248
  if (busy.rows.length === 0) {
149
249
  await conn.query(INSERT_BAKE_COLS + "VALUES (?,?,?,?,?,?,?,?,'queued',?,0,?,?,?)", params);
@@ -153,6 +253,7 @@ export class SqlImageBake {
153
253
  }
154
254
  else {
155
255
  await conn.begin();
256
+ // Defensive upsert first in case ensureSchema seeded a different poolName; ON CONFLICT keeps it idempotent.
156
257
  await conn.query("INSERT INTO image_bake_admit (pool) VALUES ($1) ON CONFLICT (pool) DO NOTHING", [this.poolName]);
157
258
  await conn.query("SELECT pool FROM image_bake_admit WHERE pool = $1 FOR UPDATE", [this.poolName]);
158
259
  const res = await conn.query(INSERT_BAKE_COLS +
@@ -167,12 +268,15 @@ export class SqlImageBake {
167
268
  await conn.rollback();
168
269
  }
169
270
  catch {
271
+ /* ignore */
170
272
  }
171
273
  throw err;
172
274
  }
173
275
  finally {
174
276
  conn.release();
175
277
  }
278
+ // 0 rows inserted ⇒ a non-dryRun bake is already in flight (the busy-check/NOT EXISTS matched under the
279
+ // sentinel lock) → busy.
176
280
  if (!inserted)
177
281
  return { created: false, bake: null };
178
282
  const bake = await this.getBake(bakeId);
@@ -188,28 +292,55 @@ export class SqlImageBake {
188
292
  const { rows } = await this.db.query(this.q(`SELECT ${SELECT_COLS} FROM image_bake WHERE idem_key = ? LIMIT 1`, `SELECT ${SELECT_COLS} FROM image_bake WHERE idem_key = $1 LIMIT 1`), [idemKey]);
189
293
  return rows[0] ? mapRow(rows[0]) : null;
190
294
  }
295
+ /**
296
+ * Find an in-flight (queued/running) bake with the identical normalized argv — so a re-submit WITHOUT an
297
+ * idempotency key still returns the live bake rather than starting a duplicate build (P2.1 "an in-flight
298
+ * identical-input bake returns the EXISTING row"). argv is the canonical normalized vector, so a JSON
299
+ * equality is the dedupe key. Newest first (the live one).
300
+ */
191
301
  async findActiveByArgv(argv) {
192
302
  const { rows } = await this.db.query(this.q(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND argv = CAST(? AS JSON) ` +
193
303
  "ORDER BY created_at DESC LIMIT 1", `SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND argv = $1::jsonb ` +
194
304
  "ORDER BY created_at DESC LIMIT 1"), [JSON.stringify(argv)]);
195
305
  return rows[0] ? mapRow(rows[0]) : null;
196
306
  }
307
+ /**
308
+ * The OLDEST in-flight (queued/running) NON-dry-run bake — the single-flight at-submit guard (P2.7). Concurrency
309
+ * is 1 (one docker daemon), so a new submit while ANY non-dry-run bake is in flight is `409 busy` UNLESS it is
310
+ * the identical-argv one (then the caller attaches to it, idempotent). dryRun bakes never take the lease, so they
311
+ * are excluded here. Oldest first = the one that holds/will-hold the lease (the build the UI should attach to).
312
+ */
197
313
  async findActiveAny() {
198
314
  const { rows } = await this.db.query(`SELECT ${SELECT_COLS} FROM image_bake WHERE status IN ('queued','running') AND dry_run = 0 ` +
199
315
  "ORDER BY created_at ASC LIMIT 1", []);
200
316
  return rows[0] ? mapRow(rows[0]) : null;
201
317
  }
318
+ /**
319
+ * The OLDEST queued bake id — the runner's long-poll claim target (P2.12). dryRun bakes never take the lease, so
320
+ * they are still discoverable here (the runner resolves them with no docker), but ordering oldest-first gives a
321
+ * stable FIFO. Returns null when the queue is empty. The actual claim is a CAS (`claimBake`), so two runners
322
+ * racing this both see the id but only one wins the conditional UPDATE — no double-build.
323
+ */
202
324
  async findNextQueuedId() {
203
325
  const { rows } = await this.db.query("SELECT bake_id FROM image_bake WHERE status = 'queued' ORDER BY created_at ASC LIMIT 1", []);
204
326
  return rows[0] ? String(rows[0].bake_id) : null;
205
327
  }
328
+ /**
329
+ * Single-flight CLAIM (P2.7) — the runner leases the next queued bake. Two CASes in one transaction:
330
+ * 1. flip the bake row `queued → running` (conditional UPDATE `WHERE status='queued'`, 1 row affected wins);
331
+ * 2. take the single-row `image_bake_lease` for the pool (INSERT, or steal an EXPIRED lease via CAS).
332
+ * Only the runner that wins BOTH gets the bake; a loser (someone else already claimed, or the lease is held by
333
+ * a live build) gets null. Mints + stores the per-bake `ingest_secret` (so a stale/rogue runner can't inject
334
+ * frames into another bake) and the initial `lease_until`. Returns the claimed row (with the secret) or null.
335
+ */
206
336
  async claimBake(bakeId, runnerId, leaseMs) {
207
337
  const conn = await this.db.connect();
208
338
  try {
209
339
  await conn.begin();
210
340
  const now = new Date();
211
341
  const leaseUntil = new Date(now.getTime() + leaseMs);
212
- const ingestSecret = randomBytes(24).toString("hex");
342
+ const ingestSecret = randomBytes(24).toString("hex"); // 48 hex chars ≤ VARCHAR(64)
343
+ // (1) claim the bake row: only a still-queued row can be claimed (single-flight on the row).
213
344
  const bakeRes = await conn.query(this.q("UPDATE image_bake SET status='running', state='PENDING', runner_id=?, ingest_secret=?, " +
214
345
  "lease_until=?, updated_at=? WHERE bake_id=? AND status='queued'", "UPDATE image_bake SET status='running', state='PENDING', runner_id=$1, ingest_secret=$2, " +
215
346
  "lease_until=$3, updated_at=$4 WHERE bake_id=$5 AND status='queued'"), [runnerId, ingestSecret, leaseUntil, now, bakeId]);
@@ -217,11 +348,24 @@ export class SqlImageBake {
217
348
  await conn.rollback();
218
349
  return null;
219
350
  }
351
+ // L1 (§P2.7): a dryRun is RESOLVE-ONLY (no docker) and MUST be parallel-safe — it NEVER takes the build lease.
352
+ // Claim the row alone (the row CAS above already single-flights it) and skip the pool-lease INSERT/steal CAS,
353
+ // so a dryRun can be claimed/resolved while a real build holds the lease (matching P2.1/P2.7/P2.14). The lease
354
+ // release in setTerminal/reapStaleBakes is a harmless no-op for a row that never took the lease. (Read dry_run
355
+ // on the same row we just claimed.)
220
356
  const dry = await conn.query(this.q("SELECT dry_run FROM image_bake WHERE bake_id=?", "SELECT dry_run FROM image_bake WHERE bake_id=$1"), [bakeId]);
221
357
  if (Boolean(dry.rows[0]?.dry_run)) {
222
358
  await conn.commit();
223
359
  return (await this.getBake(bakeId)) ?? null;
224
360
  }
361
+ // (2) take the pool lease (single-flight on the build host). 🔴 TiDB rejects a correlated sub-query that
362
+ // references the INSERT target inside ON DUPLICATE KEY UPDATE ("Can't find column …" — caught by the live
363
+ // bake, NOT the fake-pool unit test which never executes the SQL). So steal at APP-LEVEL: read the current
364
+ // lease, then INSERT (no holder) or a CAS-UPDATE (holder's lease EXPIRED **and** its bake no longer running).
365
+ // 🔴 H4(a): an `expires_at < now` lease whose holder is still `running` is an expired-but-LIVE build (a
366
+ // heartbeat gap inside the reap window) — stealing it would run TWO builds on one docker daemon, so the steal
367
+ // is gated on the holder being non-running (steal-window == reap-window; a vanished holder is also stealable).
368
+ // The CAS on (pool,bake_id,expires_at) makes the steal atomic against a concurrent refresh.
225
369
  const leaseSel = await conn.query(this.q("SELECT bake_id, expires_at FROM image_bake_lease WHERE pool=?", "SELECT bake_id, expires_at FROM image_bake_lease WHERE pool=$1"), [this.poolName]);
226
370
  const cur = leaseSel.rows[0];
227
371
  if (!cur) {
@@ -231,7 +375,7 @@ export class SqlImageBake {
231
375
  catch (err) {
232
376
  if (isDupKey(this.db.dialect, err)) {
233
377
  await conn.rollback();
234
- return null;
378
+ return null; // a concurrent claim inserted the lease first — we lose.
235
379
  }
236
380
  throw err;
237
381
  }
@@ -239,21 +383,22 @@ export class SqlImageBake {
239
383
  else {
240
384
  if (new Date(cur.expires_at).getTime() >= now.getTime()) {
241
385
  await conn.rollback();
242
- return null;
386
+ return null; // the lease is live (unexpired) — we lose.
243
387
  }
244
388
  const holderId = String(cur.bake_id ?? "");
245
389
  const holder = await conn.query(this.q("SELECT status FROM image_bake WHERE bake_id=?", "SELECT status FROM image_bake WHERE bake_id=$1"), [holderId]);
246
390
  const holderStatus = String(holder.rows[0]?.status ?? "");
247
391
  if (holderStatus === "queued" || holderStatus === "running") {
248
392
  await conn.rollback();
249
- return null;
393
+ return null; // expired-but-LIVE build — never steal (would double-bake on one daemon).
250
394
  }
251
395
  const stealRes = await conn.query(this.q("UPDATE image_bake_lease SET bake_id=?, acquired_at=?, expires_at=? WHERE pool=? AND bake_id=? AND expires_at=?", "UPDATE image_bake_lease SET bake_id=$1, acquired_at=$2, expires_at=$3 WHERE pool=$4 AND bake_id=$5 AND expires_at=$6"), [bakeId, now, leaseUntil, this.poolName, holderId, cur.expires_at]);
252
396
  if (stealRes.affected === 0) {
253
397
  await conn.rollback();
254
- return null;
398
+ return null; // a concurrent claim refreshed/stole the lease first — we lose.
255
399
  }
256
400
  }
401
+ // Double-check the lease row actually points at US (defends a same-instant race) before committing the claim.
257
402
  const check = await conn.query(this.q("SELECT bake_id FROM image_bake_lease WHERE pool=?", "SELECT bake_id FROM image_bake_lease WHERE pool=$1"), [this.poolName]);
258
403
  if (String(check.rows[0]?.bake_id ?? "") !== bakeId) {
259
404
  await conn.rollback();
@@ -267,6 +412,7 @@ export class SqlImageBake {
267
412
  await conn.rollback();
268
413
  }
269
414
  catch {
415
+ /* ignore */
270
416
  }
271
417
  throw err;
272
418
  }
@@ -274,11 +420,31 @@ export class SqlImageBake {
274
420
  conn.release();
275
421
  }
276
422
  }
423
+ /**
424
+ * Append one event. image-api is the SOLE ++seq writer: `seq = maxSeq(bakeId)+1`. The `(bakeId,line_ord)`
425
+ * UNIQUE is the at-least-once ingest dedupe (P2.10) — a runner re-sending a line it already acked races to
426
+ * INSERT and the loser is a NO-OP (the frame is already stored at its seq). Returns the seq written, or null
427
+ * when the line was a duplicate (already stored). A null `lineOrd` (image-api-synthesized frame:
428
+ * register/done/heartbeat-derived) never collides on the line-unique (NULLs are distinct in a UNIQUE index).
429
+ *
430
+ * seq allocation + the INSERT are NOT one atomic statement, so two concurrent appends could pick the same
431
+ * seq+1; the (bakeId,seq) PK then rejects the loser with a dup-key. M2: that PK collision is DISTINGUISHED from
432
+ * a benign line re-send (by the dup-key attribution channel) and RETRIED in a bounded loop with a fresh
433
+ * maxSeq+1 — never silently swallowed (which would drop a frame, incl. the terminal `done`, at a clean HTTP
434
+ * 200). A line dup is the at-least-once re-send no-op (returns null). The runner also serializes its per-bake
435
+ * pump (runner.ts), so the same-seq race is the rare burst case, not the norm.
436
+ */
277
437
  async appendEvent(bakeId, kind, data, opts = {}) {
278
438
  const lineOrd = opts.lineOrd ?? null;
279
439
  if (lineOrd !== null && (await this.lineOrdExists(bakeId, lineOrd)))
280
- return null;
440
+ return null; // fast dedupe pre-check
441
+ // PG-only: codex R14 — a `done` frame carrying a NUL in its error text would raise 22P05 and be LOST FOREVER
442
+ // (the terminal flip already cleared the ingest secret, so it cannot be retried). TiDB stores JSON verbatim.
281
443
  const payload = data == null ? null : this.db.dialect === "tidb" ? JSON.stringify(data) : pgSafeJsonStringify(data);
444
+ // The PK race is a thundering-herd: K concurrent writers all read the same maxSeq, ONE wins the (bake_id,seq)
445
+ // INSERT, the rest re-read and retry — so the unluckiest writer can lose up to ~K races in a row. A small fixed
446
+ // cap (6) under-serves a realistic ingest burst and would THROW a frame that is merely contended, not lost.
447
+ // Use a generous cap plus jittered backoff so the herd disperses instead of lock-stepping onto the same seq.
282
448
  for (let attempt = 0; attempt < 50; attempt += 1) {
283
449
  const seq = (await this.maxSeq(bakeId)) + 1;
284
450
  try {
@@ -289,8 +455,12 @@ export class SqlImageBake {
289
455
  if (!isDupKey(this.db.dialect, err))
290
456
  throw err;
291
457
  const which = dupKeyName(this.db.dialect, err);
458
+ // the line-unique lost the race (the line was concurrently stored at its own seq) → a benign at-least-once
459
+ // duplicate, NOT an error and NOT a frame loss. A null lineOrd CANNOT collide on the line-unique.
292
460
  if (which === "uq_line" && lineOrd !== null)
293
461
  return null;
462
+ // PRIMARY/(bake_id,seq) → a real same-seq race; re-read maxSeq and retry (do NOT swallow — that would
463
+ // silently drop this frame). `other` (an unnamed dup driver) is treated conservatively as retryable.
294
464
  await new Promise((r) => setTimeout(r, Math.floor(Math.random() * 5)));
295
465
  }
296
466
  }
@@ -300,16 +470,19 @@ export class SqlImageBake {
300
470
  const { rows } = await this.db.query(this.q("SELECT 1 FROM image_bake_event WHERE bake_id = ? AND line_ord = ? LIMIT 1", "SELECT 1 FROM image_bake_event WHERE bake_id = $1 AND line_ord = $2 LIMIT 1"), [bakeId, lineOrd]);
301
471
  return rows.length > 0;
302
472
  }
473
+ /** Highest event seq for a bake (0 if none) — the ++seq allocator + the resume cursor head. */
303
474
  async maxSeq(bakeId) {
304
475
  const { rows } = await this.db.query(this.q("SELECT MAX(seq) AS m FROM image_bake_event WHERE bake_id = ?", "SELECT MAX(seq) AS m FROM image_bake_event WHERE bake_id = $1"), [bakeId]);
305
476
  const m = rows[0]?.m;
306
477
  return m == null ? 0 : Number(m);
307
478
  }
479
+ /** Earliest retained event seq (0 if none) → the SSE 416 boundary (a resume below this was evicted, P2.9). */
308
480
  async retainedFrom(bakeId) {
309
481
  const { rows } = await this.db.query(this.q("SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = ?", "SELECT MIN(seq) AS m FROM image_bake_event WHERE bake_id = $1"), [bakeId]);
310
482
  const m = rows[0]?.m;
311
483
  return m == null ? 0 : Number(m);
312
484
  }
485
+ /** Events with seq strictly greater than `afterSeq` (0 for the whole stream), ascending. */
313
486
  async getEvents(bakeId, afterSeq) {
314
487
  const { rows } = await this.db.query(this.q("SELECT seq, line_ord, kind, level, data, ts FROM image_bake_event WHERE bake_id = ? AND seq > ? ORDER BY seq ASC", "SELECT seq, line_ord, kind, level, data, ts FROM image_bake_event WHERE bake_id = $1 AND seq > $2 ORDER BY seq ASC"), [bakeId, afterSeq]);
315
488
  return rows.map((r) => ({
@@ -321,12 +494,25 @@ export class SqlImageBake {
321
494
  ts: iso(r.ts),
322
495
  }));
323
496
  }
497
+ /** Coarse 4-value lifecycle transition (the column the SSE reader polls for terminality). */
324
498
  async setStatus(bakeId, status) {
325
499
  await this.db.query(this.q("UPDATE image_bake SET status = ?, updated_at = ? WHERE bake_id = ?", "UPDATE image_bake SET status = $1, updated_at = $2 WHERE bake_id = $3"), [status, new Date(), bakeId]);
326
500
  }
501
+ /** Denormalize build.sh's 8-value `state` for the UI progress bar (NEVER drives terminality, P2.3). */
327
502
  async setState(bakeId, state) {
328
503
  await this.db.query(this.q("UPDATE image_bake SET state = ?, updated_at = ? WHERE bake_id = ?", "UPDATE image_bake SET state = $1, updated_at = $2 WHERE bake_id = $3"), [state, new Date(), bakeId]);
329
504
  }
505
+ /**
506
+ * Terminal write (P2.10 step 4): flip the coarse `status` + persist all the terminal facts (digest/repo/ref/
507
+ * indexId/exitCode/error/errorCode/state/manifestSha/tag) the `done` frame and the poll row carry, AND release
508
+ * the pool lease in one transaction (a separate-statement release that failed would wedge the build host).
509
+ * Idempotent terminal write: only flips a NON-terminal row (`WHERE status IN ('queued','running')`) so a
510
+ * re-delivered terminal can't clobber the first outcome. Returns the UPDATE's affected count — 1 = THIS call won
511
+ * the queued/running→terminal transition; 0 = a terminal already exists (M3: the caller then SKIPS appending a
512
+ * contradictory second terminal `done` event, so at most one terminal `done` per bake matches the coarse row).
513
+ * 🔴 H3: null `ingest_secret` on the terminal flip so a terminal row can NEVER re-authenticate an ingest
514
+ * (a lagging/duplicate runner that still holds the secret can't inject frames into a settled bake).
515
+ */
330
516
  async setTerminal(bakeId, t) {
331
517
  let affectedRows = 0;
332
518
  await this.tx(async (conn) => {
@@ -338,6 +524,7 @@ export class SqlImageBake {
338
524
  });
339
525
  return affectedRows;
340
526
  }
527
+ /** TiDB stores the terminal facts VERBATIM (no protocol-byte class to defend against). */
341
528
  terminalParamsTidb(bakeId, t) {
342
529
  return [
343
530
  t.status,
@@ -355,6 +542,10 @@ export class SqlImageBake {
355
542
  bakeId,
356
543
  ];
357
544
  }
545
+ /** PG-ONLY protocol-byte handling. R20/R21:身份位(digest/manifestSha/repo/ref)带不可存字节=帧无效——
546
+ * **单一谓词**统管状态折叠与全部身份绑位(R21:只查两位会放 dirty repo/ref 的假 COMPLETE 过——终态单赢,
547
+ * 假成功不可逆)。事务必须提交(lease 释放/secret 清理=单飞池活性底线)。codex R13/R17-H2:NUL 错误文案
548
+ * 或脏 tag 若让终态事务回滚,lease 保持 = bake 永占单飞池,故文本位走 sanitize 而非拒绝。 */
358
549
  terminalParamsPg(bakeId, t) {
359
550
  const invalidIdentity = (t.digest != null && pgHasUnstorable(t.digest)) ||
360
551
  (t.manifestSha != null && pgHasUnstorable(t.manifestSha)) ||
@@ -376,41 +567,79 @@ export class SqlImageBake {
376
567
  bakeId,
377
568
  ];
378
569
  }
570
+ /** Set the index_id once auto-register succeeds (P2.10 — kept distinct from the terminal flip so the `done`
571
+ * frame can carry indexId even when the terminal write retries). codex R24:non-terminal 门——并发 done
572
+ * 竞态下(本请求悬在 registerBuilding 时另一 done 已赢 setTerminal)绝不把 indexId 挂回终态行(R23 折叠
573
+ * 的 FAILED/null 身份行会被复写成「null 身份却带 index 绑定」的矛盾形)。返回是否真绑定。 */
379
574
  async setIndexId(bakeId, indexId) {
380
575
  const res = await this.db.query(this.q("UPDATE image_bake SET index_id = ?, updated_at = ? WHERE bake_id = ? AND status IN ('queued','running')", "UPDATE image_bake SET index_id = $1, updated_at = $2 WHERE bake_id = $3 AND status IN ('queued','running')"), [indexId, new Date(), bakeId]);
381
576
  return res.affected >= 1;
382
577
  }
578
+ /** Heartbeat the lease forward (P2.7): the runner pushes `lease_until` (+ the lease row's `expires_at`) while
579
+ * its build child runs, so the reaper doesn't steal a LIVE build's lease. CAS on the runner owning the row +
580
+ * the bake still running. Returns whether it refreshed (false ⇒ lease lost/stolen — the runner should abort). */
383
581
  async heartbeatLease(bakeId, runnerId, leaseMs) {
384
582
  const now = new Date();
385
583
  const leaseUntil = new Date(now.getTime() + leaseMs);
386
584
  const res = await this.db.query(this.q("UPDATE image_bake SET lease_until = ?, updated_at = ? WHERE bake_id = ? AND runner_id = ? AND status = 'running'", "UPDATE image_bake SET lease_until = $1, updated_at = $2 WHERE bake_id = $3 AND runner_id = $4 AND status = 'running'"), [leaseUntil, now, bakeId, runnerId]);
387
585
  if (res.affected < 1)
388
586
  return false;
587
+ // L1: a dryRun never took the pool lease (claimBake skips it) — so it has NO `image_bake_lease` row to refresh.
588
+ // The bake-row refresh above is its only heartbeat; skip the pool-lease CAS so a healthy dryRun isn't aborted
589
+ // by an (always-zero) pool-lease no-op.
389
590
  const dry = await this.db.query(this.q("SELECT dry_run FROM image_bake WHERE bake_id = ?", "SELECT dry_run FROM image_bake WHERE bake_id = $1"), [bakeId]);
390
591
  if (Boolean(dry.rows[0]?.dry_run))
391
592
  return true;
593
+ // 🔴 H4(b): the pool-lease refresh is a REAL CAS — `WHERE pool=? AND bake_id=?` only matches while WE still
594
+ // own the lease row. If it was stolen to another bake, 0 rows affected → return false so the displaced runner
595
+ // sees leaseValid:false and aborts + kills its child (runner.ts). Without this guard the no-op UPDATE was
596
+ // silently treated as success and a displaced runner kept building (two builds, one daemon).
392
597
  const leaseRes = await this.db.query(this.q("UPDATE image_bake_lease SET expires_at = ? WHERE pool = ? AND bake_id = ?", "UPDATE image_bake_lease SET expires_at = $1 WHERE pool = $2 AND bake_id = $3"), [leaseUntil, this.poolName, bakeId]);
393
598
  return leaseRes.affected >= 1;
394
599
  }
600
+ /** Request cooperative cancel (P2.11) — durable + cross-replica; the runner's heartbeat tick polls it and kills
601
+ * the build.sh process group. Only flags a non-terminal row (terminal → 0 affected = no-op). Returns whether a
602
+ * live bake was flagged (false ⇒ already terminal → the caller answers a no-op 202). */
395
603
  async requestCancel(bakeId) {
396
604
  const res = await this.db.query(this.q("UPDATE image_bake SET cancel_requested = 1, updated_at = ? WHERE bake_id = ? AND status IN ('queued','running')", "UPDATE image_bake SET cancel_requested = 1, updated_at = $1 WHERE bake_id = $2 AND status IN ('queued','running')"), [new Date(), bakeId]);
397
605
  return res.affected > 0;
398
606
  }
607
+ /** Whether a cancel was requested (polled by the runner). */
399
608
  async isCancelRequested(bakeId) {
400
609
  const { rows } = await this.db.query(this.q("SELECT cancel_requested FROM image_bake WHERE bake_id = ?", "SELECT cancel_requested FROM image_bake WHERE bake_id = $1"), [bakeId]);
401
610
  return Boolean(rows[0]?.cancel_requested);
402
611
  }
612
+ /**
613
+ * Reaper twin of TiDBRunStore.reapStale (P2.7/§P2.6): fail any `running` bake whose lease is STALE (a crashed
614
+ * runner stopped heartbeating), append a synthetic terminal `done{failed}` so SSE readers SETTLE (never hang),
615
+ * and force-release the pool lease. `olderThanMs` must exceed >2 lease-heartbeat intervals (≈3× the heartbeat)
616
+ * so a healthy slow build is not wrongly reaped. Returns the count reaped.
617
+ *
618
+ * M3: flip the row terminal FIRST (the CAS on `status='running'`), and append the synthetic `done{failed}` ONLY
619
+ * when the flip WON (1 row affected). A victim that recovered + COMPLETEd between the SELECT and the flip
620
+ * no-ops the CAS, so we leak NO contradictory `done{failed}` into its settled SSE log. A reader that polled the
621
+ * still-running row in the tiny pre-flip gap simply re-polls (streamSseLog re-fetches) and settles on the
622
+ * eventual terminal — the winner's `done` — rather than a spurious one.
623
+ */
403
624
  async reapStaleBakes(olderThanMs) {
404
625
  const cutoff = new Date(Date.now() - olderThanMs);
405
626
  const victims = await this.db.query(this.q("SELECT bake_id FROM image_bake WHERE status = 'running' AND " + "(lease_until IS NULL OR lease_until < ?) AND updated_at < ?", "SELECT bake_id FROM image_bake WHERE status = 'running' AND " + "(lease_until IS NULL OR lease_until < $1) AND updated_at < $2"), [cutoff, cutoff]);
406
627
  let reaped = 0;
407
628
  for (const v of victims.rows) {
408
629
  const bakeId = String(v.bake_id);
630
+ // CAS the flip on still-running (a runner that recovered between the SELECT and here keeps its row).
631
+ // 🔴 H3: also null `ingest_secret` so the reaped (terminal) row can never re-authenticate an ingest — a
632
+ // lost-but-lagging runner that still holds the secret cannot inject frames into the dead bake's stream.
633
+ // codex R25: also null `index_id` atomically — a victim whose ingest died AFTER setIndexId bound but
634
+ // BEFORE setTerminal would otherwise keep a stale binding that contradicts the synthetic done below
635
+ // (indexId:null); the orphan `building` index row is lifecycle-tolerated (never published/selectable).
409
636
  const res = await this.db.query(this.q("UPDATE image_bake SET status='failed', state='FAILED', error='bake-runner lost (lease expired)', " +
410
637
  "ingest_secret=NULL, index_id=NULL, updated_at=? WHERE bake_id=? AND status='running'", "UPDATE image_bake SET status='failed', state='FAILED', error='bake-runner lost (lease expired)', " +
411
638
  "ingest_secret=NULL, index_id=NULL, updated_at=$1 WHERE bake_id=$2 AND status='running'"), [new Date(), bakeId]);
412
639
  if (res.affected === 1) {
413
640
  reaped += 1;
641
+ // M3: we WON the flip — only now append the synthetic terminal so a reader settles on a `done` that matches
642
+ // the coarse row. A recovered+COMPLETEd victim never reaches here (the CAS no-op'd), so no contradiction.
414
643
  try {
415
644
  await this.appendEvent(bakeId, "done", {
416
645
  status: "FAILED",
@@ -424,12 +653,14 @@ export class SqlImageBake {
424
653
  });
425
654
  }
426
655
  catch {
656
+ /* best-effort — the row is already terminal; a reader sees status!=running and re-fetches. */
427
657
  }
428
658
  await this.db.query(this.q("DELETE FROM image_bake_lease WHERE pool = ? AND bake_id = ?", "DELETE FROM image_bake_lease WHERE pool = $1 AND bake_id = $2"), [this.poolName, bakeId]);
429
659
  }
430
660
  }
431
661
  return reaped;
432
662
  }
663
+ /** Run statements atomically; rolls back on failure (mirrors TiDBRunStore.tx). */
433
664
  async tx(fn) {
434
665
  const conn = await this.db.connect();
435
666
  try {
@@ -442,6 +673,7 @@ export class SqlImageBake {
442
673
  await conn.rollback();
443
674
  }
444
675
  catch {
676
+ /* ignore */
445
677
  }
446
678
  throw err;
447
679
  }
@@ -450,11 +682,13 @@ export class SqlImageBake {
450
682
  }
451
683
  }
452
684
  }
685
+ /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
453
686
  export class TiDBImageBake extends SqlImageBake {
454
687
  constructor(pool, poolName = "standard") {
455
688
  super(mysqlDriver(pool), poolName);
456
689
  }
457
690
  }
691
+ /** PostgreSQL binding — historical class name + ctor shape preserved. */
458
692
  export class PgImageBake extends SqlImageBake {
459
693
  constructor(pool, poolName = "standard") {
460
694
  super(pgDriver(pool), poolName);
@@ -4,29 +4,75 @@ import { type ImageStatus, type ImageIndexEntry, type ImageIndexUpsert, type Ima
4
4
  import { type SqlDriver } from "./sql-driver.js";
5
5
  export type { ImageNestedBuildMode, ImageCapabilities, ImagePodContract, ImageStatus, ImageVisibility, ImageIndexEntry, ImageIndexUpsert, ImageViewer, ImageListFilter, ImageRow, } from "./store-contracts.js";
6
6
  export { mapImageRow as mapRow } from "./store-contracts.js";
7
+ /** PG translation of the `sandbox_image_index` DDL in tidb-pool.ts (JSON→JSONB, DATETIME(3)→TIMESTAMPTZ(3),
8
+ * TINYINT(1)→SMALLINT(布尔列 PG 映射归一,clay 批 2026-07-26 —— 两方言读回同为 number,写入 0/1,
9
+ * 读侧 `!!` 保持,出口 JS 类型不变), inline UNIQUE KEY/KEY → separate CREATE UNIQUE INDEX / CREATE INDEX). */
7
10
  export declare const PG_IMAGE_INDEX_SCHEMA: string[];
11
+ /** Idempotent self-contained schema apply (for the integration test; central aggregation is done separately). */
8
12
  export declare function ensureSchema(pool: PgPool | PoolClient): Promise<void>;
13
+ /** Dual-dialect INDEX store. See the file header for the dialect-delta ledger. */
9
14
  export declare class SqlImageIndex {
10
15
  protected readonly db: SqlDriver;
11
16
  constructor(db: SqlDriver);
17
+ /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
12
18
  private q;
19
+ /** Stable index id = sha256(repo@digest) — identity is the digest, not the mutable tag. */
13
20
  static idFor(repo: string, digest: string): string;
21
+ /** Register or update an index row (idempotent on repo+digest). Returns the row id. */
14
22
  upsert(e: ImageIndexUpsert): Promise<string>;
23
+ /**
24
+ * STATUS-MONOTONIC register (IMAGE-API-DESIGN.md §P2.10 MUST-FIX) — the auto-register path the bake runner uses.
25
+ *
26
+ * `upsert` does an UNCONDITIONAL upsert: a re-ingested `done` (at-least-once from the runner) or a P2 bake of
27
+ * a digest P3 already PROMOTED would flip a live `published` row back to `building`, and `latestPublished`
28
+ * (`WHERE status='published'`) then DE-PUBLISHES a live, in-use image → sandbox-create 404s. This guarded path
29
+ * NEVER downgrades a `published` row: on a fresh row it inserts (`status:'building'` by default); on an
30
+ * existing row it updates the manifest projection + bumps `updated_at` but writes `status` only when the row
31
+ * isn't already `published` — a late/duplicate `done` for an already-promoted digest is a no-op on the
32
+ * lifecycle, never a regression. Returns the row id.
33
+ *
34
+ * Distinct from the unconditional `upsert` (kept for the seed + the operator `/v1/images/register` path); this
35
+ * is the bake auto-register's ONLY write into the index. Default status is `building` (P2 registers
36
+ * node-local/quarantine `building` artifacts; P3's verify gate is what flips `building → published`).
37
+ *
38
+ * The `IF(...published...)` (TiDB) / `CASE WHEN...published...` (PG) guards are the monotonic de-publish
39
+ * defense: once a row is `published` and live, a re-register must NOT downgrade `status` NOR flip the
40
+ * columns `latestPublished` filters on. wq64gmm5e: guarding only `status` left `visibility`/`tenant_id`
41
+ * (and `signed`) overwritable, so a re-register with visibility=private/tenant or a different tenant_id
42
+ * silently dropped a live row out of every viewer's `latestPublished` — an effective de-publish. ALL FOUR
43
+ * (status/visibility/tenant_id/signed) are gated identically in BOTH dialects; non-published rows still
44
+ * take the freshest projection.
45
+ */
15
46
  registerBuilding(e: ImageIndexUpsert): Promise<string>;
16
47
  getById(id: string): Promise<ImageIndexEntry | null>;
48
+ /** By immutable digest — what a running sandbox was built from. */
17
49
  getByDigest(digest: string): Promise<ImageIndexEntry | null>;
50
+ /** The newest PUBLISHED, viewer-visible digest for a profile — the `select` resolution.
51
+ * 🔴 genuinely-divergent shape (not just SQL text): TiDB's `visibilityClauseTidb` needs no placeholder
52
+ * index (anonymous `?`); PG's `visibilityClausePg` threads a running `$n` index the caller continues
53
+ * from. Also the null-safe ORDER BY trick differs ((build_date IS NULL) vs NULLS LAST) — same semantics. */
18
54
  latestPublished(profile: string, viewer: ImageViewer): Promise<ImageIndexEntry | null>;
55
+ /**
56
+ * Paginated, viewer-scoped catalog query (the UI's list). Keyset cursor on (created_at, id) desc.
57
+ * 🔴 genuinely-divergent shape: the PG arm threads an explicit `$n` index through every optional predicate
58
+ * (node-pg has no anonymous placeholder); the TiDB arm just appends `?` in call order. Kept as two full
59
+ * branches rather than a shared placeholder-numbering helper (sql-driver.ts STEP 3/4) — a reviewer sees each
60
+ * dialect's real query shape, not a generated abstraction over it.
61
+ */
19
62
  list(filter: ImageListFilter): Promise<{
20
63
  entries: ImageIndexEntry[];
21
64
  nextCursor: string | null;
22
65
  }>;
66
+ /** Lifecycle transition (atomic-publish: building→published, or →failed/deprecated). */
23
67
  setStatus(id: string, status: ImageStatus, opts?: {
24
68
  supersedes?: string | null;
25
69
  }): Promise<void>;
26
70
  }
71
+ /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
27
72
  export declare class TiDBImageIndex extends SqlImageIndex {
28
73
  constructor(pool: MySqlPool);
29
74
  }
75
+ /** PostgreSQL binding — historical class name + ctor shape preserved. */
30
76
  export declare class PgImageIndex extends SqlImageIndex {
31
77
  constructor(pool: PgPool);
32
78
  }