@sema-agent/server 1.323.0 → 2.0.1

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 (565) 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/budget-tracing.d.ts +48 -0
  46. package/dist/boot/budget-tracing.js +86 -0
  47. package/dist/boot/config-center.d.ts +62 -0
  48. package/dist/boot/config-center.js +995 -0
  49. package/dist/boot/coordinators.d.ts +33 -0
  50. package/dist/boot/coordinators.js +97 -0
  51. package/dist/boot/execution-env.d.ts +26 -0
  52. package/dist/boot/execution-env.js +370 -0
  53. package/dist/boot/leader.d.ts +27 -0
  54. package/dist/boot/leader.js +81 -0
  55. package/dist/boot/reapers.d.ts +53 -0
  56. package/dist/boot/reapers.js +252 -0
  57. package/dist/boot/resolve-spec.d.ts +70 -0
  58. package/dist/boot/resolve-spec.js +1072 -0
  59. package/dist/boot/runner-deps.d.ts +101 -0
  60. package/dist/boot/runner-deps.js +343 -0
  61. package/dist/boot/runtime-caps.d.ts +21 -0
  62. package/dist/boot/runtime-caps.js +62 -0
  63. package/dist/boot/session-faces.d.ts +57 -0
  64. package/dist/boot/session-faces.js +157 -0
  65. package/dist/boot/shutdown.d.ts +50 -0
  66. package/dist/boot/shutdown.js +129 -0
  67. package/dist/boot/stores.d.ts +32 -0
  68. package/dist/boot/stores.js +361 -0
  69. package/dist/boot/workflow-orchestration.d.ts +46 -0
  70. package/dist/boot/workflow-orchestration.js +150 -0
  71. package/dist/boot-reclaim.d.ts +19 -0
  72. package/dist/boot-reclaim.js +20 -0
  73. package/dist/brain.d.ts +25 -0
  74. package/dist/brain.js +61 -1
  75. package/dist/budget.d.ts +66 -0
  76. package/dist/budget.js +128 -4
  77. package/dist/capabilities/builtin-tools.d.ts +3 -0
  78. package/dist/capabilities/builtin-tools.js +3 -0
  79. package/dist/capabilities/center-plugins.d.ts +17 -1
  80. package/dist/capabilities/center-plugins.js +45 -4
  81. package/dist/capabilities/center-prompts.d.ts +46 -0
  82. package/dist/capabilities/center-prompts.js +57 -3
  83. package/dist/capabilities/code-review-council.d.ts +28 -0
  84. package/dist/capabilities/code-review-council.js +40 -7
  85. package/dist/capabilities/collab-wire.d.ts +2 -0
  86. package/dist/capabilities/collab-workflows.d.ts +7 -0
  87. package/dist/capabilities/collab-workflows.js +44 -1
  88. package/dist/capabilities/oa-tools.d.ts +6 -0
  89. package/dist/capabilities/oa-tools.js +10 -0
  90. package/dist/capabilities/prompt.d.ts +14 -0
  91. package/dist/capabilities/prompt.js +14 -0
  92. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  93. package/dist/capabilities/prompts/code-review.js +14 -0
  94. package/dist/capabilities/prompts/identity.d.ts +6 -0
  95. package/dist/capabilities/prompts/identity.js +6 -0
  96. package/dist/capabilities/prompts/team.d.ts +4 -0
  97. package/dist/capabilities/prompts/team.js +4 -0
  98. package/dist/capabilities/repo-tools.d.ts +11 -0
  99. package/dist/capabilities/repo-tools.js +8 -0
  100. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  101. package/dist/capabilities/sandbox-file-send.js +91 -3
  102. package/dist/capabilities/scenarios.d.ts +130 -0
  103. package/dist/capabilities/scenarios.js +180 -7
  104. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  105. package/dist/capabilities/select-environment-tool.js +30 -0
  106. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  107. package/dist/capabilities/send-user-file-tool.js +65 -1
  108. package/dist/capabilities/skills.d.ts +29 -0
  109. package/dist/capabilities/skills.js +13 -2
  110. package/dist/capabilities/team.d.ts +22 -0
  111. package/dist/capabilities/team.js +22 -2
  112. package/dist/capabilities/tool-defer.d.ts +26 -0
  113. package/dist/capabilities/tool-defer.js +3 -0
  114. package/dist/config-center/apply-effective.d.ts +65 -0
  115. package/dist/config-center/apply-effective.js +204 -12
  116. package/dist/config-center/http-client.d.ts +34 -0
  117. package/dist/config-center/http-client.js +68 -0
  118. package/dist/config-center/restart-signal.d.ts +22 -0
  119. package/dist/config-center/restart-signal.js +39 -0
  120. package/dist/config-center/skills-mcp.d.ts +20 -1
  121. package/dist/config-center/skills-mcp.js +42 -7
  122. package/dist/config-center/types.d.ts +92 -1
  123. package/dist/config-lkg.d.ts +12 -0
  124. package/dist/config-lkg.js +41 -0
  125. package/dist/config-provider.d.ts +78 -0
  126. package/dist/config-provider.js +165 -2
  127. package/dist/config-types.d.ts +585 -1
  128. package/dist/config.d.ts +78 -2
  129. package/dist/config.js +874 -301
  130. package/dist/degenerate-instrument.d.ts +38 -0
  131. package/dist/degenerate-instrument.js +55 -3
  132. package/dist/elicitation.d.ts +39 -0
  133. package/dist/elicitation.js +87 -3
  134. package/dist/env-facts.d.ts +61 -0
  135. package/dist/env-facts.js +73 -7
  136. package/dist/fleet/fleet-bus.d.ts +185 -0
  137. package/dist/fleet/fleet-bus.js +333 -27
  138. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  139. package/dist/fleet/subagent-tail-bus.js +33 -2
  140. package/dist/fleet-client.d.ts +38 -0
  141. package/dist/fleet-client.js +44 -9
  142. package/dist/fleet-lease.d.ts +46 -0
  143. package/dist/fleet-lease.js +73 -5
  144. package/dist/hooks/branch-transcript.d.ts +8 -0
  145. package/dist/hooks/branch-transcript.js +33 -0
  146. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  147. package/dist/hooks/cc-stop-prompt.js +43 -1
  148. package/dist/hooks/hook-llm.d.ts +15 -0
  149. package/dist/hooks/hook-llm.js +47 -0
  150. package/dist/hooks/hook-runner.d.ts +89 -0
  151. package/dist/hooks/hook-runner.js +327 -18
  152. package/dist/http/idempotency.d.ts +31 -0
  153. package/dist/http/idempotency.js +34 -0
  154. package/dist/http/principal-gate.d.ts +17 -0
  155. package/dist/http/principal-gate.js +35 -3
  156. package/dist/http/route-ctx.d.ts +36 -0
  157. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  158. package/dist/http/routes/approvals-assistant.js +253 -23
  159. package/dist/http/routes/attachments.js +22 -4
  160. package/dist/http/routes/capabilities.d.ts +7 -0
  161. package/dist/http/routes/capabilities.js +202 -1
  162. package/dist/http/routes/fleet.d.ts +6 -0
  163. package/dist/http/routes/fleet.js +94 -5
  164. package/dist/http/routes/images.d.ts +17 -0
  165. package/dist/http/routes/images.js +222 -14
  166. package/dist/http/routes/leader.d.ts +7 -0
  167. package/dist/http/routes/leader.js +10 -0
  168. package/dist/http/routes/memory-policy.d.ts +6 -0
  169. package/dist/http/routes/memory-policy.js +38 -6
  170. package/dist/http/routes/notify-wake.d.ts +10 -0
  171. package/dist/http/routes/notify-wake.js +19 -3
  172. package/dist/http/routes/observability.d.ts +6 -0
  173. package/dist/http/routes/observability.js +19 -2
  174. package/dist/http/routes/runs.d.ts +15 -0
  175. package/dist/http/routes/runs.js +464 -41
  176. package/dist/http/routes/session-sync.d.ts +15 -0
  177. package/dist/http/routes/session-sync.js +229 -19
  178. package/dist/http/routes/sessions-list.d.ts +6 -0
  179. package/dist/http/routes/sessions-list.js +35 -4
  180. package/dist/http/routes/sessions.d.ts +14 -0
  181. package/dist/http/routes/sessions.js +176 -31
  182. package/dist/http/routes/side-query.d.ts +8 -0
  183. package/dist/http/routes/side-query.js +26 -1
  184. package/dist/http/routes/tasks.d.ts +18 -0
  185. package/dist/http/routes/tasks.js +501 -38
  186. package/dist/http/routes/trace-usage.d.ts +10 -0
  187. package/dist/http/routes/trace-usage.js +66 -14
  188. package/dist/http/routes/workflows.d.ts +12 -0
  189. package/dist/http/routes/workflows.js +180 -19
  190. package/dist/http/run-meta.d.ts +11 -0
  191. package/dist/http/run-meta.js +6 -0
  192. package/dist/http/send.d.ts +19 -0
  193. package/dist/http/send.js +11 -0
  194. package/dist/http/server.d.ts +255 -0
  195. package/dist/http/server.js +921 -75
  196. package/dist/http/sse-log.d.ts +15 -0
  197. package/dist/http/sse-log.js +16 -0
  198. package/dist/http/tar.d.ts +9 -0
  199. package/dist/http/tar.js +21 -5
  200. package/dist/http/wire-gate.d.ts +9 -0
  201. package/dist/http/wire-gate.js +9 -0
  202. package/dist/http/wire-types.d.ts +187 -0
  203. package/dist/http/workspace-content.d.ts +8 -0
  204. package/dist/http/workspace-content.js +10 -0
  205. package/dist/images/bake-validate.d.ts +63 -0
  206. package/dist/images/bake-validate.js +71 -1
  207. package/dist/images/manifest.d.ts +19 -0
  208. package/dist/images/manifest.js +6 -1
  209. package/dist/index.d.ts +1 -5
  210. package/dist/index.js +10 -5
  211. package/dist/key-resolver.d.ts +23 -0
  212. package/dist/key-resolver.js +27 -2
  213. package/dist/leader/diffout.d.ts +23 -0
  214. package/dist/leader/diffout.js +8 -1
  215. package/dist/leader/diffup.d.ts +32 -0
  216. package/dist/leader/diffup.js +43 -0
  217. package/dist/leader/endpoint.d.ts +20 -0
  218. package/dist/leader/endpoint.js +25 -2
  219. package/dist/leader/fanout.d.ts +60 -0
  220. package/dist/leader/fanout.js +29 -3
  221. package/dist/leader/grader-env-factory.d.ts +74 -0
  222. package/dist/leader/grader-env-factory.js +62 -3
  223. package/dist/leader/leader.d.ts +97 -0
  224. package/dist/leader/leader.js +151 -4
  225. package/dist/leader/merge.d.ts +94 -0
  226. package/dist/leader/merge.js +112 -9
  227. package/dist/leader/planner.d.ts +52 -0
  228. package/dist/leader/planner.js +47 -3
  229. package/dist/leader/repair-oracle.d.ts +61 -0
  230. package/dist/leader/repair-oracle.js +59 -1
  231. package/dist/leader/repair-wire.d.ts +78 -0
  232. package/dist/leader/repair-wire.js +79 -2
  233. package/dist/leader/wire.d.ts +79 -0
  234. package/dist/leader/wire.js +255 -8
  235. package/dist/lsp/e2b-bridge.d.ts +16 -0
  236. package/dist/lsp/e2b-bridge.js +32 -4
  237. package/dist/lsp/e2b-manager.d.ts +10 -0
  238. package/dist/lsp/e2b-manager.js +18 -2
  239. package/dist/lsp/lsp-frames.d.ts +12 -0
  240. package/dist/lsp/lsp-frames.js +12 -0
  241. package/dist/lsp/manager.d.ts +12 -0
  242. package/dist/lsp/manager.js +35 -2
  243. package/dist/lsp/types.d.ts +10 -0
  244. package/dist/lsp/ws-transport.d.ts +12 -0
  245. package/dist/lsp/ws-transport.js +53 -5
  246. package/dist/lsp-evict.d.ts +14 -0
  247. package/dist/lsp-evict.js +15 -1
  248. package/dist/main.js +530 -2129
  249. package/dist/memory-export.d.ts +1 -0
  250. package/dist/memory-export.js +4 -0
  251. package/dist/memory-scope.d.ts +49 -0
  252. package/dist/memory-scope.js +87 -4
  253. package/dist/memory-sync-client.d.ts +24 -0
  254. package/dist/memory-sync-client.js +41 -2
  255. package/dist/memory-sync.d.ts +23 -0
  256. package/dist/memory-sync.js +70 -1
  257. package/dist/model-select.d.ts +39 -0
  258. package/dist/model-select.js +42 -1
  259. package/dist/observability/cost-quota.d.ts +20 -0
  260. package/dist/observability/cost-quota.js +4 -0
  261. package/dist/observability/cost-taxonomy.d.ts +45 -0
  262. package/dist/observability/cost-taxonomy.js +34 -0
  263. package/dist/observability/logger.d.ts +1 -0
  264. package/dist/observability/logger.js +6 -0
  265. package/dist/observability/metrics.d.ts +13 -0
  266. package/dist/observability/metrics.js +63 -0
  267. package/dist/observability/otel-exporter.d.ts +9 -0
  268. package/dist/observability/otel-exporter.js +11 -3
  269. package/dist/observability/principal-context.d.ts +9 -0
  270. package/dist/observability/principal-context.js +9 -0
  271. package/dist/observability/prompt-manifest.d.ts +37 -0
  272. package/dist/observability/prompt-manifest.js +35 -1
  273. package/dist/observability/rate-limit.d.ts +10 -0
  274. package/dist/observability/rate-limit.js +1 -0
  275. package/dist/observability/tool-trace.d.ts +24 -0
  276. package/dist/observability/tool-trace.js +41 -1
  277. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  278. package/dist/orchestration/hardened-vm-runner.js +119 -4
  279. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  280. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  281. package/dist/orchestration/hardened-vm-worker.js +27 -0
  282. package/dist/orchestration/subagent-steer.d.ts +44 -0
  283. package/dist/orchestration/subagent-steer.js +45 -1
  284. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  285. package/dist/orchestration/workflow-agent-steer.js +80 -1
  286. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  287. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  288. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  289. package/dist/orchestration/workflow-notify-journal.js +187 -13
  290. package/dist/parked-decide.d.ts +61 -0
  291. package/dist/parked-decide.js +58 -1
  292. package/dist/per-task-image.d.ts +54 -0
  293. package/dist/per-task-image.js +41 -0
  294. package/dist/plan-cache-probe.d.ts +35 -0
  295. package/dist/plan-cache-probe.js +18 -3
  296. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  297. package/dist/plugins/approval-exemption-store.js +28 -2
  298. package/dist/plugins/approval-store-sql.d.ts +64 -0
  299. package/dist/plugins/approval-store-sql.js +32 -0
  300. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  301. package/dist/plugins/background-agent-store-sql.js +91 -4
  302. package/dist/plugins/background-shell-support.d.ts +98 -0
  303. package/dist/plugins/background-shell-support.js +117 -13
  304. package/dist/plugins/blob-backend.d.ts +104 -0
  305. package/dist/plugins/blob-backend.js +163 -6
  306. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  307. package/dist/plugins/breaker-state-sql.js +46 -8
  308. package/dist/plugins/caching-session-store.d.ts +42 -0
  309. package/dist/plugins/caching-session-store.js +63 -3
  310. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  311. package/dist/plugins/checkpoint-store-sql.js +239 -11
  312. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  313. package/dist/plugins/file-outcome-sink.js +9 -0
  314. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  315. package/dist/plugins/file-resume-anchor-store.js +43 -4
  316. package/dist/plugins/file-run-store.d.ts +90 -0
  317. package/dist/plugins/file-run-store.js +145 -13
  318. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  319. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  320. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  321. package/dist/plugins/file-workflow-journal-store.js +10 -0
  322. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  323. package/dist/plugins/fork-routing-session-store.js +51 -3
  324. package/dist/plugins/host-platform.d.ts +69 -0
  325. package/dist/plugins/host-platform.js +81 -2
  326. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  327. package/dist/plugins/image-bake-store-sql.js +241 -7
  328. package/dist/plugins/image-index-sql.d.ts +46 -0
  329. package/dist/plugins/image-index-sql.js +113 -4
  330. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  331. package/dist/plugins/k8s-bg-scripts.js +88 -4
  332. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  333. package/dist/plugins/k8s-exec-protocol.js +41 -0
  334. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  335. package/dist/plugins/local-checkpoint-store.js +86 -7
  336. package/dist/plugins/local-session-store.d.ts +157 -0
  337. package/dist/plugins/local-session-store.js +289 -34
  338. package/dist/plugins/local-task-attachment-store.js +14 -2
  339. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  340. package/dist/plugins/mailbox-store-sql.js +39 -6
  341. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  342. package/dist/plugins/memory-engine-pg.js +122 -4
  343. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  344. package/dist/plugins/memory-engine-tidb.js +115 -5
  345. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  346. package/dist/plugins/memory-engine-vector-util.js +5 -0
  347. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  348. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  349. package/dist/plugins/memory-run-store.d.ts +63 -0
  350. package/dist/plugins/memory-run-store.js +73 -8
  351. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  352. package/dist/plugins/memory-session-policy-store.js +17 -0
  353. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  354. package/dist/plugins/memory-sync-store-pg.js +45 -2
  355. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  356. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  357. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  358. package/dist/plugins/outcome-ledger-sql.js +97 -3
  359. package/dist/plugins/pg-approval-store.d.ts +7 -0
  360. package/dist/plugins/pg-approval-store.js +7 -0
  361. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  362. package/dist/plugins/pg-breaker-state.js +6 -0
  363. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  364. package/dist/plugins/pg-checkpoint-store.js +8 -0
  365. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  366. package/dist/plugins/pg-cost-quota.js +6 -0
  367. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  368. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  369. package/dist/plugins/pg-image-bake.d.ts +9 -0
  370. package/dist/plugins/pg-image-bake.js +9 -0
  371. package/dist/plugins/pg-image-index.d.ts +9 -0
  372. package/dist/plugins/pg-image-index.js +9 -0
  373. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  374. package/dist/plugins/pg-outcome-ledger.js +9 -0
  375. package/dist/plugins/pg-pool.d.ts +50 -0
  376. package/dist/plugins/pg-pool.js +78 -0
  377. package/dist/plugins/pg-query.d.ts +11 -0
  378. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  379. package/dist/plugins/pg-rate-limiter.js +12 -2
  380. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  381. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  382. package/dist/plugins/pg-run-store.d.ts +7 -0
  383. package/dist/plugins/pg-run-store.js +7 -0
  384. package/dist/plugins/pg-safe-json.d.ts +13 -0
  385. package/dist/plugins/pg-safe-json.js +40 -4
  386. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  387. package/dist/plugins/pg-session-policy-store.js +5 -0
  388. package/dist/plugins/pg-session-storage.d.ts +72 -0
  389. package/dist/plugins/pg-session-storage.js +152 -20
  390. package/dist/plugins/pg-session-store.d.ts +10 -0
  391. package/dist/plugins/pg-session-store.js +10 -0
  392. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  393. package/dist/plugins/pg-tool-result-store.js +7 -0
  394. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  395. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  396. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  397. package/dist/plugins/pg-workflow-run-store.js +7 -0
  398. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  399. package/dist/plugins/posix-shell-fs.js +31 -1
  400. package/dist/plugins/remote-env-adb.d.ts +25 -0
  401. package/dist/plugins/remote-env-adb.js +95 -12
  402. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  403. package/dist/plugins/remote-env-e2b.js +371 -40
  404. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  405. package/dist/plugins/remote-env-file-error.js +37 -0
  406. package/dist/plugins/remote-env-host.d.ts +151 -0
  407. package/dist/plugins/remote-env-host.js +468 -70
  408. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  409. package/dist/plugins/remote-env-k8s.js +318 -32
  410. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  411. package/dist/plugins/remote-env-local-docker.js +149 -24
  412. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  413. package/dist/plugins/remote-env-ssh.js +117 -20
  414. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  415. package/dist/plugins/remote-scratchpad.js +32 -2
  416. package/dist/plugins/remote-shell.d.ts +22 -0
  417. package/dist/plugins/remote-shell.js +27 -1
  418. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  419. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  420. package/dist/plugins/roster-store-sql.d.ts +27 -0
  421. package/dist/plugins/roster-store-sql.js +54 -0
  422. package/dist/plugins/run-store-sql.d.ts +194 -0
  423. package/dist/plugins/run-store-sql.js +210 -9
  424. package/dist/plugins/s3-presign.d.ts +20 -0
  425. package/dist/plugins/s3-presign.js +49 -2
  426. package/dist/plugins/scheduler-support.d.ts +12 -0
  427. package/dist/plugins/scheduler-support.js +75 -3
  428. package/dist/plugins/send-file-ledger.d.ts +15 -0
  429. package/dist/plugins/send-file-ledger.js +53 -4
  430. package/dist/plugins/send-user-file.d.ts +77 -0
  431. package/dist/plugins/send-user-file.js +92 -4
  432. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  433. package/dist/plugins/session-policy-store-sql.js +75 -3
  434. package/dist/plugins/session-store.d.ts +45 -0
  435. package/dist/plugins/session-store.js +51 -0
  436. package/dist/plugins/sql-driver.d.ts +77 -0
  437. package/dist/plugins/sql-driver.js +4 -0
  438. package/dist/plugins/sql-escape.d.ts +8 -0
  439. package/dist/plugins/sql-escape.js +8 -0
  440. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  441. package/dist/plugins/sql-row-helpers.js +25 -0
  442. package/dist/plugins/store-backend.d.ts +103 -0
  443. package/dist/plugins/store-backend.js +120 -27
  444. package/dist/plugins/store-contracts.d.ts +62 -0
  445. package/dist/plugins/store-contracts.js +3 -0
  446. package/dist/plugins/task-attachment-store.d.ts +60 -0
  447. package/dist/plugins/task-attachment-store.js +49 -3
  448. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  449. package/dist/plugins/task-list-store-sql.js +21 -0
  450. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  451. package/dist/plugins/tidb-approval-store.js +6 -0
  452. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  453. package/dist/plugins/tidb-breaker-state.js +5 -0
  454. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  455. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  456. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  457. package/dist/plugins/tidb-cost-quota.js +1 -0
  458. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  459. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  460. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  461. package/dist/plugins/tidb-image-bake.js +8 -0
  462. package/dist/plugins/tidb-image-index.d.ts +8 -0
  463. package/dist/plugins/tidb-image-index.js +8 -0
  464. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  465. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  466. package/dist/plugins/tidb-pool.d.ts +56 -0
  467. package/dist/plugins/tidb-pool.js +181 -1
  468. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  469. package/dist/plugins/tidb-rate-limiter.js +5 -2
  470. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  471. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  472. package/dist/plugins/tidb-run-store.d.ts +7 -0
  473. package/dist/plugins/tidb-run-store.js +7 -0
  474. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  475. package/dist/plugins/tidb-session-policy-store.js +5 -0
  476. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  477. package/dist/plugins/tidb-session-storage.js +62 -3
  478. package/dist/plugins/tidb-session-store.d.ts +142 -0
  479. package/dist/plugins/tidb-session-store.js +264 -16
  480. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  481. package/dist/plugins/tidb-tool-result-store.js +7 -0
  482. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  483. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  484. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  485. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  486. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  487. package/dist/plugins/tool-result-store-sql.js +38 -2
  488. package/dist/plugins/web-search.d.ts +35 -0
  489. package/dist/plugins/web-search.js +45 -9
  490. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  491. package/dist/plugins/workflow-journal-limits.js +10 -0
  492. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  493. package/dist/plugins/workflow-journal-store-sql.js +58 -2
  494. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  495. package/dist/plugins/workflow-run-store-sql.js +84 -6
  496. package/dist/plugins/worktree-isolation.d.ts +45 -0
  497. package/dist/plugins/worktree-isolation.js +105 -6
  498. package/dist/plugins/write-behind-counter.d.ts +56 -1
  499. package/dist/plugins/write-behind-counter.js +53 -14
  500. package/dist/principal-jwt.d.ts +24 -0
  501. package/dist/principal-jwt.js +60 -5
  502. package/dist/project-memory.d.ts +22 -0
  503. package/dist/project-memory.js +144 -15
  504. package/dist/prompts-domain-validate.d.ts +28 -0
  505. package/dist/prompts-domain-validate.js +42 -1
  506. package/dist/question.d.ts +33 -0
  507. package/dist/question.js +69 -2
  508. package/dist/resource-suspend.d.ts +21 -0
  509. package/dist/resource-suspend.js +16 -0
  510. package/dist/router/route-orchestration.d.ts +78 -0
  511. package/dist/router/route-orchestration.js +77 -0
  512. package/dist/run-local.d.ts +32 -0
  513. package/dist/run-local.js +172 -8
  514. package/dist/runs.d.ts +189 -4
  515. package/dist/runs.js +465 -24
  516. package/dist/runtime-caps-resolver.d.ts +106 -0
  517. package/dist/runtime-caps-resolver.js +89 -4
  518. package/dist/runtime-governance.d.ts +84 -0
  519. package/dist/runtime-governance.js +127 -1
  520. package/dist/sandbox-pkg-source.d.ts +23 -0
  521. package/dist/sandbox-pkg-source.js +37 -0
  522. package/dist/sealed-key.d.ts +54 -0
  523. package/dist/sealed-key.js +68 -3
  524. package/dist/security.d.ts +108 -0
  525. package/dist/security.js +145 -6
  526. package/dist/sema-registry.d.ts +21 -0
  527. package/dist/sema-registry.js +21 -0
  528. package/dist/session-leaf-bus.d.ts +31 -0
  529. package/dist/session-leaf-bus.js +32 -0
  530. package/dist/session-sync-content.d.ts +46 -0
  531. package/dist/session-sync-content.js +47 -1
  532. package/dist/session-sync-kernel.d.ts +89 -0
  533. package/dist/session-sync-kernel.js +48 -3
  534. package/dist/session-sync.d.ts +125 -0
  535. package/dist/session-sync.js +181 -8
  536. package/dist/session-titler.d.ts +26 -0
  537. package/dist/session-titler.js +41 -7
  538. package/dist/session-watch.d.ts +53 -1
  539. package/dist/session-watch.js +73 -9
  540. package/dist/sighup-idle.d.ts +30 -0
  541. package/dist/sighup-idle.js +12 -1
  542. package/dist/spec-fields.d.ts +91 -3
  543. package/dist/spec-fields.js +116 -4
  544. package/dist/task-cwd.d.ts +50 -0
  545. package/dist/task-cwd.js +68 -1
  546. package/dist/task-mcp.d.ts +47 -0
  547. package/dist/task-mcp.js +29 -0
  548. package/dist/task-settings.d.ts +175 -0
  549. package/dist/task-settings.js +226 -11
  550. package/dist/task-workflow.d.ts +67 -0
  551. package/dist/task-workflow.js +75 -8
  552. package/dist/tool-approval.d.ts +91 -0
  553. package/dist/tool-approval.js +215 -0
  554. package/dist/trace/artifacts.d.ts +9 -0
  555. package/dist/trace/artifacts.js +30 -5
  556. package/dist/trace/core-keyset-guard.d.ts +18 -0
  557. package/dist/trace/ledger-sink.d.ts +43 -0
  558. package/dist/trace/ledger-sink.js +64 -5
  559. package/dist/trace/project.d.ts +133 -0
  560. package/dist/trace/project.js +189 -2
  561. package/dist/trace/redact.d.ts +19 -0
  562. package/dist/trace/redact.js +66 -9
  563. package/dist/usage-analytics.d.ts +26 -0
  564. package/dist/usage-analytics.js +26 -3
  565. package/package.json +1 -1
@@ -1,28 +1,85 @@
1
+ /**
2
+ * Durable `CheckpointStore` (core 1.67 durable-checkpoint / design/45) — SINGLE-FILE DUAL-DIALECT
3
+ * (design/158 A12 定型半场). ONE implementation, TWO dialects; the historical `TiDBCheckpointStore` /
4
+ * `PgCheckpointStore` class names survive as thin ctor subclasses so every consumer (store-backend.ts,
5
+ * local-checkpoint-store.ts, the chaos/ownership/blackbox suites) is untouched.
6
+ *
7
+ * This is the durable backend that makes a policy `ask` survive cross-replica/crash/time as a suspended
8
+ * task an operator can `resume` from any instance. It implements core's `CheckpointStore` (put create-once
9
+ * / get / atomic CAS resolve / reopen / reap), modeled on the proven approval-store CAS, plus two concerns
10
+ * the core interface doesn't own but the service does (DURABLE-CHECKPOINT-PLAN §4.4a):
11
+ * - `checkpoint_ctx` (sessionId-keyed): the service-side scenario rebuild inputs for `resume`'s taskConfig —
12
+ * core's `put(token, cp)` can't carry it (it's service "profile", not the generic core checkpoint).
13
+ * - operator-queue helpers (`listPending` / `findPendingTokenBySession`) that surface NON-secret fields and
14
+ * map a session → its pending token INTERNALLY — the capability token never leaves the service (token-as-auth).
15
+ *
16
+ * ── Dialect deltas, kept EXPLICIT ────────────────────────────────────────────────────────────────────
17
+ * - `?` placeholders vs `$n` (and the OPTIONAL `AND rev = ?` / `AND rev = $5` OCC tail)
18
+ * - `ON DUPLICATE KEY UPDATE … VALUES(col)` vs `ON CONFLICT (session_id) DO UPDATE SET … EXCLUDED.col`
19
+ * - dup-key errno `1062` (ER_DUP_ENTRY) vs SQLSTATE `23505`
20
+ * - null-safe owner compare: `<=>` vs `IS NOT DISTINCT FROM` — AND the parameter arity that follows
21
+ * (`?` cannot be reused, so the TiDB deleteBySession binds sessionId TWICE; PG reuses `$1`)
22
+ * - affectedRows vs rowCount (via SqlDriver)
23
+ * - JSON binding: TiDB `JSON.stringify` verbatim vs PG `pgProtocolJsonStringify` + `$n::jsonb` casts.
24
+ * PG's envelope is LOSSLESS (codex R4-H1): tool_input is the operator's review surface and must agree
25
+ * with `checkpoint.pendingAction.args` at the NUL position — redact/truncate is the display contract,
26
+ * byte rewriting is not.
27
+ * - schema ownership: TiDB DDL in tidb-pool.ts, PG DDL centrally in pg-pool.ts (neither store creates tables).
28
+ */
1
29
  import { createHash } from "node:crypto";
2
30
  import { CheckpointError, validatePendingSteer, checkpointVersionOf, winnerFromOutcome, summarizeCheckpoint, MAX_SUPPORTED_CHECKPOINT_VERSION, } from "@sema-agent/core";
3
31
  import { redactDeep } from "../trace/redact.js";
4
32
  import { pgProtocolJsonStringify } from "./pg-safe-json.js";
5
33
  import { parseJsonStrict as parseJson } from "./sql-row-helpers.js";
6
34
  import { mysqlDriver, pgDriver } from "./sql-driver.js";
35
+ /** Cap the persisted/served pending-approval args so a pathological tool payload can't bloat the checkpoint row
36
+ * or the operator-queue response (the BFF renders this in an approval card; a few KB is plenty). */
7
37
  const MAX_TOOL_INPUT_CHARS = 8192;
38
+ /**
39
+ * design/80 D-1 (§3 invariant #3 — crash-safe reaper backstop): an ABSOLUTE upper bound on a pending
40
+ * checkpoint's lifetime, stamped at put() into `terminal_at` INDEPENDENT of the per-approval `deadline`. The
41
+ * SLA-timer (D-D) does the fine, per-gate-kind resolve-deny; THIS coarse backstop ensures even a pending row
42
+ * with a NULL `deadline` (no approval TTL configured) is eventually GC'd if the SLA service dies — closing a
43
+ * forever-leak of a never-resolved suspension (the current `reap`/`reapExpired` only catch non-NULL deadlines).
44
+ * Generous (default 30d) so it never pre-empts a legitimately long-lived human/irreversible_ask gate;
45
+ * env-overridable. Clamped ≥1min so a misconfig can't expire live suspensions instantly.
46
+ */
8
47
  export const TERMINAL_BACKSTOP_MS = Math.max(60_000, Number(process.env.APPROVAL_TERMINAL_BACKSTOP_MS) || 30 * 86_400_000);
9
- export const TERMINAL_GRACE_MS = 3_600_000;
48
+ /**
49
+ * design/80 D-D (adversarial fix): the crash-safe `terminal_at` backstop must fall STRICTLY AFTER any SLA
50
+ * `deadline`, never AT it. `terminal_at = max(createdAt+backstop, deadline)` made the two coincide whenever an
51
+ * operator tuned APPROVAL_TERMINAL_BACKSTOP_MS at/below the SLA — and reapExpired's terminal_at-branch (which
52
+ * has NO gate_kind filter) then abort-EXPIRED a human/irreversible_ask gate in the SAME tick the deny-sweep
53
+ * wanted to gracefully DENY it, racing it away. Adding this grace to the deadline term guarantees the deny-sweep
54
+ * at least this window of clean ticks before the absolute backstop can fire. Far smaller than the backstop, so
55
+ * it never meaningfully delays the eventual crash-safe GC.
56
+ */
57
+ export const TERMINAL_GRACE_MS = 3_600_000; // exported for the LOCAL twin's read-time terminal_at derivation (anti-drift) // 1h — many reaper intervals of deny-sweep runway past the SLA deadline
58
+ /**
59
+ * The capability token IS the resume credential (token-as-auth) — anyone who reads it can impersonate a
60
+ * resume, so it must never reach the logs (which fan out to a log-aggregation pipeline). For the diagnostic
61
+ * resume-read-path log we emit a stable, non-reversible fingerprint instead, enough to correlate get↔resolve.
62
+ */
10
63
  export function tokenFingerprint(token) {
11
64
  return "sha256:" + createHash("sha256").update(token).digest("hex").slice(0, 12);
12
65
  }
66
+ /** Redact + size-bound a pending tool call's args for persistence/serving. JSON-roundtrips so the stored shape
67
+ * matches what the operator queue serves; oversized payloads collapse to a marker rather than bloating the row.
68
+ * Exported for the LOCAL twin (local-checkpoint-store.ts) so the operator-queue projection can't drift. */
13
69
  export function boundedToolInput(args) {
14
70
  if (args === undefined)
15
71
  return null;
16
72
  const redacted = redactDeep(args);
17
73
  const json = JSON.stringify(redacted);
18
74
  if (json === undefined)
19
- return null;
75
+ return null; // non-serializable (e.g. a bare function) — nothing to surface
20
76
  if (json.length > MAX_TOOL_INPUT_CHARS)
21
77
  return { truncated: true, bytes: json.length };
22
78
  return redacted;
23
79
  }
24
- const DUP_ENTRY = 1062;
80
+ const DUP_ENTRY = 1062; // MySQL/TiDB ER_DUP_ENTRY
25
81
  const PG_UNIQUE_VIOLATION = "23505";
82
+ /** Dual-dialect durable CheckpointStore. See the file header for the dialect-delta ledger. */
26
83
  export class SqlCheckpointStore {
27
84
  db;
28
85
  logger;
@@ -30,9 +87,12 @@ export class SqlCheckpointStore {
30
87
  this.db = db;
31
88
  this.logger = logger;
32
89
  }
90
+ /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */
33
91
  q(tidb, pg) {
34
92
  return this.db.dialect === "tidb" ? tidb : pg;
35
93
  }
94
+ /** JSON column binding: TiDB stores the string verbatim; PG goes through the LOSSLESS protocol envelope
95
+ * (R4-H1 — the operator's review surface must agree with the executed args at the NUL position). */
36
96
  json(value, label) {
37
97
  return this.db.dialect === "tidb" ? JSON.stringify(value) : pgProtocolJsonStringify(value, label);
38
98
  }
@@ -41,9 +101,12 @@ export class SqlCheckpointStore {
41
101
  ? e?.errno === DUP_ENTRY
42
102
  : e?.code === PG_UNIQUE_VIOLATION;
43
103
  }
104
+ /** Create-once. core mints the token (`mintCheckpointToken`) and calls this during suspend. */
44
105
  async put(token, cp) {
45
106
  const pa = cp.pendingAction;
46
- const version = checkpointVersionOf(cp);
107
+ const version = checkpointVersionOf(cp); // core helper: cp.version ?? 0 (1.67/1.68-era no-field = legacy 0)
108
+ // Persist the (redacted, bounded) tool args alongside so the operator queue can serve the approval-card
109
+ // payload without an N+1 trace.turns fetch. Stored as a stringified JSON column value.
47
110
  const toolInput = boundedToolInput(pa?.args);
48
111
  try {
49
112
  await this.db.query(this.q("INSERT INTO checkpoint (token, scope, session_id, version, status, tool_name, tool_call_id, tool_input, checkpoint, deadline, created_at, terminal_at, gate_kind, bound_input_hash, risk_descriptor) " +
@@ -59,14 +122,15 @@ export class SqlCheckpointStore {
59
122
  this.json(cp, "checkpoint"),
60
123
  cp.deadline ?? null,
61
124
  cp.createdAt,
62
- Math.max(cp.createdAt + TERMINAL_BACKSTOP_MS, (cp.deadline ?? 0) + TERMINAL_GRACE_MS),
63
- cp.gate?.kind ?? null,
64
- pa?.boundInputHash ?? null,
65
- ((g) => (g?.riskDescriptor ? this.json(g.riskDescriptor, "risk descriptor") : null))(cp.gate),
125
+ Math.max(cp.createdAt + TERMINAL_BACKSTOP_MS, (cp.deadline ?? 0) + TERMINAL_GRACE_MS), // D-1 §3 inv#3 backstop — STRICTLY after any SLA deadline (grace) so the terminal_at-branch never races the D-D deny-sweep, and never pre-empts an operator's longer TTL
126
+ cp.gate?.kind ?? null, // D-D SLA split: human/irreversible_ask deadline → resolve-deny; others → expire
127
+ pa?.boundInputHash ?? null, // D-1: the opaque hash the portal must echo on /decide (surfaced via listPending so the TOCTOU binding is reachable)
128
+ ((g) => (g?.riskDescriptor ? this.json(g.riskDescriptor, "risk descriptor") : null))(cp.gate), // riskDescriptor inbox: stamp core's INERT descriptor for triage-sort
66
129
  ]);
67
130
  }
68
131
  catch (e) {
69
132
  if (this.isDupKey(e)) {
133
+ // a reused token would clobber a live suspension — never a silent overwrite (checkpoint-store contract)
70
134
  throw new CheckpointError("checkpoint.already_exists", `checkpoint token already exists`);
71
135
  }
72
136
  throw e;
@@ -77,22 +141,37 @@ export class SqlCheckpointStore {
77
141
  const r = rows[0];
78
142
  if (!r)
79
143
  return null;
144
+ // Forward-compat guard: never hand a too-new checkpoint to a resume that can't
145
+ // parse it. Compare with `<= MAX` (never `=== CURRENT`) using core's exported constant so this
146
+ // stays aligned when core bumps the format. This durable-store get-path is the STRONGEST guard (protects a
147
+ // worker that doesn't check version itself); core's resume has a second pre-CAS guard.
80
148
  const version = Number(r.version);
81
149
  if (version > MAX_SUPPORTED_CHECKPOINT_VERSION) {
82
150
  throw new Error(`checkpoint format version ${version} is newer than this build supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION}) — upgrade the service to resume it`);
83
151
  }
84
152
  const cp = parseJson(r.checkpoint);
153
+ // the COLUMN status is authoritative (resolve/reap update it, not the blob) — reflect it on the returned cp.
85
154
  cp.status = r.status;
155
+ // design/80 D-1: surface the reopen-by-reason + OCC fields off the AUTHORITATIVE columns (the blob is the
156
+ // suspend-time snapshot; resolve/reopen mutate only the columns). Core's resume reads these to validate an
157
+ // env_failed re-resume against the persisted winner, and to require the `rev` it observed is still live (OCC).
158
+ // resolvedOutcome is DERIVED from the stored full outcome (winnerFromOutcome → {boundCallId,decision,
159
+ // updatedInput?}), so it is preserved across a reopen for free (reopen leaves `outcome` untouched).
86
160
  const outcomeRaw = parseJson(r.outcome);
87
161
  if (outcomeRaw)
88
162
  cp.resolvedOutcome = winnerFromOutcome(outcomeRaw);
89
163
  cp.reopenReason = r.reopen_reason ?? undefined;
90
- cp.rev = r.rev == null ? 0 : Number(r.rev);
164
+ cp.rev = r.rev == null ? 0 : Number(r.rev); // legacy/pre-migration rows ⇒ 0 (the .d.ts "Absent ⇒ legacy 0")
165
+ // design/80 D-A: overlay the parked steer off the AUTHORITATIVE column (set by setPendingSteer AFTER the
166
+ // suspend-time blob was written) so core injects it on resume. The blob never carries it.
91
167
  const steerRaw = parseJson(r.pending_steer);
92
168
  if (steerRaw && cp.state)
93
169
  cp.state.pendingSteer = steerRaw;
170
+ // Diagnostic: log what the REAL runner.resume store.get returns for the remote
171
+ // workspaceHandle (the isolated round-trip test can't prove the real-path get). undefined here ⇒ store/get
172
+ // drops it; present ⇒ core's 910→614 threading drops it. Cheap, on the resume read-path only.
94
173
  this.logger?.info?.("checkpoint_get", {
95
- tokenFp: tokenFingerprint(token),
174
+ tokenFp: tokenFingerprint(token), // NEVER the raw token — it is the resume credential (token-as-auth)
96
175
  status: cp.status,
97
176
  version,
98
177
  hasWorkspaceHandle: !!cp.state?.workspaceHandle,
@@ -100,6 +179,25 @@ export class SqlCheckpointStore {
100
179
  });
101
180
  return cp;
102
181
  }
182
+ /**
183
+ * Atomic CAS: pending → resolved, scoped. Returns true for the single winner, false if already
184
+ * resolved/expired (AlreadyResolved → caller no-ops, never re-executes). `outcome` is persisted atomically
185
+ * with the flip (R20 #1 — a future v2 resumable-resume can recover a crash-after-CAS; v1 runner write-only).
186
+ *
187
+ * design/80 D-1 (atomicity): the winner-persist (the full `outcome` is stored; the winner
188
+ * {boundCallId,decision,updatedInput?} is derived from it on read) + the `rev` bump happen ATOMICALLY in the
189
+ * SAME UPDATE as the pending→resolved CAS — one statement, no get→write window. When the caller passes the
190
+ * `rev` it observed at get() (ResolveExpectation), the CAS additionally requires the LIVE rev to still equal
191
+ * it: ANY intervening resolve/reopen cycle bumps rev, so a cycle in the get→resolve window loses the CAS
192
+ * (returns false → core re-inspects → checkpoint.reopened_concurrently). Omitting `expect` keeps the legacy
193
+ * unconditional CAS (a first resume of a never-reopened checkpoint is unconstrained by rev).
194
+ * 🔴 reopen_reason = NULL on a winning resolve — parity with the core reference InMemoryCheckpointStore
195
+ * (MINOR-12: "a freshly-resolved row carries no stale reopen reason"). Without this, an env_failed reopen
196
+ * followed by a correct winner-replay resolve leaves reopen_reason='env_failed' stale on the now-resolved
197
+ * row; a later DUPLICATE resume of that token would hit core's pre-CAS env_failed winner-replay guard
198
+ * against the stale reason and throw checkpoint.reopen_revote (which the service classifies RETRIABLE →
199
+ * re-parks the run suspended) instead of the terminal checkpoint.already_resolved — stranding the run.
200
+ */
103
201
  async resolve(token, scope, outcome, expect) {
104
202
  const params = [this.json(outcome, "checkpoint outcome"), Date.now(), token, scope];
105
203
  if (expect)
@@ -109,35 +207,103 @@ export class SqlCheckpointStore {
109
207
  (expect ? " AND rev = $5" : "")), params);
110
208
  return res.affected === 1;
111
209
  }
210
+ /**
211
+ * Inverse of `resolve` (core `CheckpointStore.reopen`): CAS `resolved → pending`. Returns the CAS winner
212
+ * (true = was resolved → now pending again). Compensates a post-CAS env-restore failure
213
+ * (`resume.env_failed`): `resumeStream` consumes the checkpoint BEFORE `resumeVM`, so a restore failure
214
+ * would otherwise strand the suspended work as `resolved` + unfindable (`findPendingTokenBySession` only
215
+ * matches `pending`). Reopening lets a retry re-resume the SAME work.
216
+ *
217
+ * design/80 D-1 (reopen-by-reason): the `reason` distinguishes the two triggers (both fire at core's
218
+ * runtask.ts) so the re-resume can validate per reason (§3 inv #1) — `env_failed` = the action is still
219
+ * valid, the re-resume MUST replay the persisted winner (core rejects a different decision with
220
+ * `checkpoint.reopen_revote`); `tool_unavailable` = the action could not run and may now be stale, so a
221
+ * FRESH decision is allowed (P-7). We RECORD the reason on the row and bump `rev` (so a concurrent resume's
222
+ * resolve CAS loses — atomicity). 🔴 We DELIBERATELY leave the persisted `outcome` UNTOUCHED (NOT stale junk
223
+ * as a pre-D-1 reopen treated it): `resolvedOutcome` derives from it on get(), and an `env_failed` re-resume
224
+ * is validated against it — clearing it would defeat the winner-replay invariant.
225
+ */
112
226
  async reopen(token, scope, reason) {
113
227
  const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'pending', reopen_reason = ?, rev = rev + 1 WHERE token = ? AND scope = ? AND status = 'resolved'", "UPDATE checkpoint SET status = 'pending', reopen_reason = $1, rev = rev + 1 WHERE token = $2 AND scope = $3 AND status = 'resolved'"), [reason, token, scope]);
114
228
  return res.affected === 1;
115
229
  }
230
+ /**
231
+ * design/80 D-A (durable steering): park a steer message on a STILL-PENDING checkpoint (last-writer-wins),
232
+ * surfaced via get() as `state.pendingSteer` and injected on resume. CAS on `status='pending'` (+ scope) so a
233
+ * steer NEVER lands on a resolved/expired checkpoint and NEVER touches status/the resolve path (§3 inv #4).
234
+ * Returns the CAS winner (`true` = landed on a live pending checkpoint; `false` = no pending row to steer).
235
+ */
116
236
  async setPendingSteer(token, scope, steer) {
117
- const clean = validatePendingSteer(steer);
237
+ // M2(接入审计 2026-07-08):契约把 validatePendingSteer 定为【每个 impl 内部】必跑的 fail-closed 不变式
238
+ // ("EVERY CheckpointStore.setPendingSteer impl runs [it] so the reject is IDENTICAL across the in-memory and
239
+ // durable backends")——core 自家 File/InMemory/Pg 三 impl 全有;此前这里裸写 UPDATE=靠调用方自觉(HTTP 层
240
+ // 三处前置校验只是碰巧全覆盖,下一个内部调用方不一定记得)。clean = 冻结 {text,trusted} 两字段拷贝
241
+ // (调用方对象上的多余字段绝不落库,契约同款)。
242
+ const clean = validatePendingSteer(steer); // throws CheckpointError(steering.invalid_content) — per-impl 一致拒绝
118
243
  const res = await this.db.query(this.q("UPDATE checkpoint SET pending_steer = ? WHERE token = ? AND scope = ? AND status = 'pending'", "UPDATE checkpoint SET pending_steer = $1 WHERE token = $2 AND scope = $3 AND status = 'pending'"), [this.json(clean, "pending steer"), token, scope]);
119
244
  return res.affected > 0;
120
245
  }
246
+ /**
247
+ * CAS-expire a SINGLE checkpoint by token (core 1.71 `CheckpointStore.expire`, for `TaskStream.destroy`'s
248
+ * suspended-worker reap — core [R33] {delta}). `pending → expired`, returns the CAS winner (true = we
249
+ * expired it; false = already resolved/expired). Same UPDATE as `reap`, keyed by **token** not deadline.
250
+ * It races the SAME `pending` row as `resolve`, so the two are mutually exclusive: destroy() must `expire`
251
+ * (fence) BEFORE destroying the env — if a concurrent resume already won `resolve`, expire returns false and
252
+ * destroy leaves the env for that resume; if expire wins, the resume's resolve fails → never resume-to-dead-env.
253
+ * Uses `expired` (not a `resolve`-deny) so a CANCELLED checkpoint never pollutes resolved-count / outcome.
254
+ */
121
255
  async expire(token, scope) {
122
256
  const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at = ? WHERE token = ? AND scope = ? AND status = 'pending'", "UPDATE checkpoint SET status = 'expired', decided_at = $1 WHERE token = $2 AND scope = $3 AND status = 'pending'"), [Date.now(), token, scope]);
123
257
  return res.affected === 1;
124
258
  }
259
+ /** Interface reap: CAS-expire pending checkpoints in `scope` past `cutoff`. Returns count. */
125
260
  async reap(scope, cutoff) {
126
261
  const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at = ? WHERE scope = ? AND status = 'pending' AND deadline IS NOT NULL AND deadline <= ?", "UPDATE checkpoint SET status = 'expired', decided_at = $1 WHERE scope = $2 AND status = 'pending' AND deadline IS NOT NULL AND deadline <= $3"), [Date.now(), scope, cutoff]);
127
262
  return res.affected;
128
263
  }
264
+ /**
265
+ * GLOBAL sweep for the service's per-replica TTL reaper (expiry isn't tenant-
266
+ * sensitive — only `resolve` is scoped). Idempotent across replicas (DB serializes; no election). Returns count.
267
+ * Called with `cutoff = Date.now()` (deadline/terminal_at are ABSOLUTE epoch-ms), so it expires any pending row
268
+ * whose per-approval `deadline` OR its design/80 D-1 §3-inv#3 `terminal_at` crash-safe backstop has passed —
269
+ * the latter closes the forever-leak of a pending row with a NULL `deadline` (no approval TTL was configured).
270
+ *
271
+ * design/80 D-D: the deadline-branch EXPIRES (≈ abort) every kind EXCEPT a tool-approval human/irreversible_ask
272
+ * gate — those are resolve-DENIED gracefully by the SLA deny-sweep (listExpiredApprovalGates →
273
+ * resumeCheckpoint(deny)), so expiring them here too would race + lose the graceful denial. 🔴 A durable
274
+ * AskUserQuestion ALSO mints gate.kind='human' (no question-specific kind in core) — but DENYING a question is
275
+ * incoherent (the model gets a "denied" tool-result, not an answer), so it is carved BACK INTO the expire path
276
+ * (COALESCE(tool_name,'')='AskUserQuestion') to abort-expire on timeout instead. Legacy rows (gate_kind NULL)
277
+ * stay on the expire path. The terminal_at-branch is the crash-safe backstop for ANY kind (incl. a human gate
278
+ * whose deny-resume keeps failing) — it always abort-expires past the absolute cap (which is now STRICTLY after
279
+ * the deadline, so it never races the deny-sweep at the deadline instant).
280
+ */
129
281
  async reapExpired(cutoff) {
130
282
  const res = await this.db.query(this.q("UPDATE checkpoint SET status = 'expired', decided_at = ? " +
131
283
  "WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= ? AND (gate_kind IS NULL OR gate_kind NOT IN ('human','irreversible_ask') OR COALESCE(tool_name,'') = 'AskUserQuestion')) OR (terminal_at IS NOT NULL AND terminal_at <= ?))", "UPDATE checkpoint SET status = 'expired', decided_at = $1 " +
132
284
  "WHERE status = 'pending' AND ((deadline IS NOT NULL AND deadline <= $2 AND (gate_kind IS NULL OR gate_kind NOT IN ('human','irreversible_ask') OR COALESCE(tool_name,'') = 'AskUserQuestion')) OR (terminal_at IS NOT NULL AND terminal_at <= $3))"), [Date.now(), cutoff, cutoff]);
133
285
  return res.affected;
134
286
  }
287
+ /**
288
+ * design/80 D-D (SLA-timer): the sessions of pending human/irreversible_ask checkpoints past their `deadline`
289
+ * (the per-approval SLA). The reaper resolve-DENIES each (re-resume with decision=deny → the model continues
290
+ * with the denial) — graceful, vs the abort that expire() gives resource_limit/needs_review. Excludes legacy
291
+ * rows (gate_kind NULL — they predate the split and fall through to reapExpired's expire path). LIMIT bounds
292
+ * the per-tick deny-resume work (each is a model run). Scope is read off the COMMITTED row (invariant #4).
293
+ * 🔴 EXCLUDES AskUserQuestion gates (which also mint gate.kind='human'): denying a question is incoherent —
294
+ * the model would get a "denied" tool-result instead of an answer — so a timed-out durable question falls to
295
+ * reapExpired's abort-expire path instead of this graceful-deny path.
296
+ */
135
297
  async listExpiredApprovalGates(cutoff, limit = 100) {
136
298
  const { rows } = await this.db.query(this.q("SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ('human','irreversible_ask') " +
137
299
  "AND COALESCE(tool_name,'') <> 'AskUserQuestion' AND deadline IS NOT NULL AND deadline <= ? ORDER BY deadline ASC LIMIT ?", "SELECT session_id, scope FROM checkpoint WHERE status = 'pending' AND gate_kind IN ('human','irreversible_ask') " +
138
300
  "AND COALESCE(tool_name,'') <> 'AskUserQuestion' AND deadline IS NOT NULL AND deadline <= $1 ORDER BY deadline ASC LIMIT $2"), [cutoff, limit]);
139
301
  return rows.map((r) => ({ sessionId: String(r.session_id), scope: String(r.scope) }));
140
302
  }
303
+ // ─────────────────────── operator approval queue (non-secret; token stays internal) ───────────────────────
304
+ /** Pending checkpoints for the operator queue — NON-secret fields only (never the token). `taskId` is
305
+ * joined from task_active (the JOIN KEY to the run/trace — a suspended run KEEPS its
306
+ * session claim, so the join is live for every pending row; null only in pathological windows). */
141
307
  async listPending(scope) {
142
308
  const base = "SELECT c.session_id, c.scope, c.tool_name, c.tool_call_id, c.tool_input, c.bound_input_hash, c.risk_descriptor, c.created_at, c.deadline, ta.task_id " +
143
309
  "FROM checkpoint c LEFT JOIN task_active ta ON ta.session_id = c.session_id WHERE c.status='pending'";
@@ -151,21 +317,41 @@ export class SqlCheckpointStore {
151
317
  scope: String(r.scope),
152
318
  toolName: r.tool_name ?? null,
153
319
  toolCallId,
320
+ // D-1: boundCallId === the pending tool call id; boundInputHash is the server-minted opaque hash. The
321
+ // portal echoes BOTH on /decide so the resume binds to the exact action shown (TOCTOU guard reachable).
154
322
  boundCallId: toolCallId,
155
323
  boundInputHash: r.bound_input_hash ?? null,
156
324
  taskId: r.task_id ?? null,
325
+ // Both drivers return the JSON column already parsed; null for pre-migration rows. PG 库内恒干净
326
+ // (拒绝式)——直读即审阅面=执行面。
157
327
  input: r.tool_input ?? null,
158
328
  createdAt: Number(r.created_at),
159
329
  deadline: r.deadline == null ? null : Number(r.deadline),
160
330
  riskDescriptor: parseJson(r.risk_descriptor),
161
331
  };
162
332
  });
333
+ // riskDescriptor inbox: triage-sort by severity DESC (highest-risk asks first), then oldest-first within a
334
+ // tier. A row with no descriptor (pre-migration / non-gate) sorts to severity 0 (after the descriptored ones).
163
335
  return out.sort((a, b) => (b.riskDescriptor?.severity ?? 0) - (a.riskDescriptor?.severity ?? 0) || a.createdAt - b.createdAt);
164
336
  }
337
+ /**
338
+ * design/80 #2 (scheduler seam, core 1.105): a scope's PENDING checkpoints as lightweight
339
+ * {@link CheckpointSummary} rows in ONE query — the cross-task triage view a supervisor scheduler needs
340
+ * (gate kind + risk severity + budget spent + deadline per suspended task), no N+1 `get`s. The projection is
341
+ * core's shared {@link summarizeCheckpoint} run over the persisted blob (the `checkpoint` column = the same full
342
+ * {@link Checkpoint} `get()` parses), so this stays byte-identical to core's InMemory/Pg/File impls. Order =
343
+ * created_at ASC; callers (the inbox/scheduler) sort by severity. The COLUMN `status` is authoritative (the blob
344
+ * is the suspend-time snapshot), so it overrides the blob's status before the summary is derived.
345
+ *
346
+ * LIMIT bounds the fan-out (review w16yqkkxv): a triage view never needs more than a few — 500 is a generous
347
+ * ceiling that still protects memory/latency if a scope ever accumulates pathologically many pending gates.
348
+ */
165
349
  async listByScope(scope) {
166
350
  const { rows } = await this.db.query(this.q("SELECT checkpoint, status FROM checkpoint WHERE status = 'pending' AND scope = ? ORDER BY created_at ASC LIMIT 500", "SELECT checkpoint, status FROM checkpoint WHERE status = 'pending' AND scope = $1 ORDER BY created_at ASC LIMIT 500"), [scope]);
167
351
  const out = [];
168
352
  for (const r of rows) {
353
+ // A corrupt/missing blob skips THAT row rather than crashing the whole scheduler view (review w16yqkkxv):
354
+ // unlike get() (a single-token read where a null blob is a hard error), a bulk triage list must degrade.
169
355
  const cp = parseJson(r.checkpoint);
170
356
  if (!cp)
171
357
  continue;
@@ -174,18 +360,31 @@ export class SqlCheckpointStore {
174
360
  }
175
361
  return out;
176
362
  }
363
+ /**
364
+ * Map a session (the non-secret handle the operator decides by) → its single pending checkpoint token,
365
+ * INTERNALLY. A suspended session has exactly one pending checkpoint. `scope` is enforced so an operator
366
+ * can only resolve within its own tenant. Returns null if there is no pending checkpoint for that session/scope.
367
+ */
177
368
  async findPendingTokenBySession(sessionId, scope) {
178
369
  const { rows } = scope
179
370
  ? await this.db.query(this.q("SELECT token FROM checkpoint WHERE session_id=? AND scope=? AND status='pending' LIMIT 1", "SELECT token FROM checkpoint WHERE session_id=$1 AND scope=$2 AND status='pending' LIMIT 1"), [sessionId, scope])
180
371
  : await this.db.query(this.q("SELECT token FROM checkpoint WHERE session_id=? AND status='pending' LIMIT 1", "SELECT token FROM checkpoint WHERE session_id=$1 AND status='pending' LIMIT 1"), [sessionId]);
181
372
  return rows[0] ? String(rows[0].token) : null;
182
373
  }
374
+ /**
375
+ * The owner SCOPE of a session's pending checkpoint (the multi-tenant key === the owner principal in the
376
+ * BFF/non-operator flow, the same key listPending filters by). For the /decide owner-gate: a non-operator
377
+ * caller may only decide a checkpoint in their OWN scope. Returns `undefined` (no pending row), `null`
378
+ * (anonymous/dev session — open), or the owner scope string.
379
+ */
183
380
  async peekPendingScope(sessionId) {
184
381
  const { rows } = await this.db.query(this.q("SELECT scope FROM checkpoint WHERE session_id=? AND status='pending' LIMIT 1", "SELECT scope FROM checkpoint WHERE session_id=$1 AND status='pending' LIMIT 1"), [sessionId]);
185
382
  if (!rows[0])
186
383
  return undefined;
187
384
  return rows[0].scope ?? null;
188
385
  }
386
+ // ─────────────────────── checkpoint_ctx (service rebuild inputs; sessionId-keyed) ───────────────────────
387
+ /** Upsert the resume rebuild inputs for a session (written at SUBMIT when durable mode is on). */
189
388
  async putCtx(sessionId, ctx) {
190
389
  await this.db.query(this.q("INSERT INTO checkpoint_ctx (session_id, ctx, updated_at) VALUES (?,?,?) " +
191
390
  "ON DUPLICATE KEY UPDATE ctx = VALUES(ctx), updated_at = VALUES(updated_at)", "INSERT INTO checkpoint_ctx (session_id, ctx, updated_at) VALUES ($1,$2::jsonb,$3) " +
@@ -195,6 +394,17 @@ export class SqlCheckpointStore {
195
394
  const { rows } = await this.db.query(this.q("SELECT ctx FROM checkpoint_ctx WHERE session_id=?", "SELECT ctx FROM checkpoint_ctx WHERE session_id=$1"), [sessionId]);
196
395
  return rows[0] ? parseJson(rows[0].ctx) : null;
197
396
  }
397
+ /**
398
+ * GC ctx rows whose lifecycle is over (bound to the checkpoint, don't leak):
399
+ * a session with NO pending checkpoint, NO active run, AND not touched since `cutoff`.
400
+ *
401
+ * The extra `task_active` guard closes a multi-gate window: between a resume's CAS (pending→resolved) and
402
+ * the task hitting its NEXT gate (writing a new pending checkpoint), the session has no pending checkpoint —
403
+ * yet `putCtx` only stamps `updated_at` at submit, so the ctx could be older than `cutoff` and was being
404
+ * reaped, making the next resume 409 ("resume context missing"). `task_active` holds the session lock for
405
+ * the WHOLE submit→last-resume-complete span (running OR suspended), so excluding sessions with an active
406
+ * claim keeps the ctx exactly as long as a run still needs it, while still GC'ing it once the run is terminal.
407
+ */
198
408
  async reapCtx(cutoff) {
199
409
  const res = await this.db.query(this.q("DELETE FROM checkpoint_ctx WHERE updated_at < ? " +
200
410
  "AND session_id NOT IN (SELECT session_id FROM checkpoint WHERE status='pending') " +
@@ -203,6 +413,22 @@ export class SqlCheckpointStore {
203
413
  "AND session_id NOT IN (SELECT session_id FROM task_active)"), [cutoff]);
204
414
  return res.affected;
205
415
  }
416
+ /**
417
+ * E21 (§0.5 session delete) — purge ALL checkpoint state for one session: the durable checkpoint rows
418
+ * (suspended-task gates) AND their service-side resume-context rows (checkpoint_ctx). Keyed on the
419
+ * `session_id` column both tables carry. Idempotent (returns the count of `checkpoint` rows removed).
420
+ * Unconditional by design: a session being deleted has no resumable future, so any pending gate is moot.
421
+ *
422
+ * `owner` is a single-DB-fleet defense-in-depth guard (adversarial-review MEDIUM). Neither table carries an
423
+ * owner column, so the guard threads through `session_meta.owner` via an EXISTS sub-select (null-safe compare:
424
+ * `<=>` on TiDB, `IS NOT DISTINCT FROM` on PG). The purge coordinator runs this BEFORE deleting session_meta,
425
+ * so the meta row (with its authoritative owner) is still present here — a wrong-owner caller's EXISTS fails
426
+ * and purges nothing. `owner=null` (legacy/dev, never threaded by the route) keeps the historical
427
+ * unconditional behaviour via the null-safe match.
428
+ *
429
+ * 🔴 Placeholder ARITY differs, not just the syntax: `?` cannot be reused, so the TiDB arm binds `sessionId`
430
+ * TWICE (row key + guard sub-select) while PG reuses `$1`.
431
+ */
206
432
  async deleteBySession(sessionId, owner) {
207
433
  const ownerGuard = this.q("EXISTS (SELECT 1 FROM session_meta sm WHERE sm.session_id = ? AND sm.owner <=> ?)", "EXISTS (SELECT 1 FROM session_meta sm WHERE sm.session_id = $1 AND sm.owner IS NOT DISTINCT FROM $2)");
208
434
  const params = this.db.dialect === "tidb" ? [sessionId, sessionId, owner] : [sessionId, owner];
@@ -211,11 +437,13 @@ export class SqlCheckpointStore {
211
437
  return res.affected;
212
438
  }
213
439
  }
440
+ /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */
214
441
  export class TiDBCheckpointStore extends SqlCheckpointStore {
215
442
  constructor(pool, logger) {
216
443
  super(mysqlDriver(pool), logger);
217
444
  }
218
445
  }
446
+ /** PostgreSQL binding — historical class name + ctor shape preserved. */
219
447
  export class PgCheckpointStore extends SqlCheckpointStore {
220
448
  constructor(pool, logger) {
221
449
  super(pgDriver(pool), logger);
@@ -1,4 +1,8 @@
1
1
  import type { TaskOutcome as CoreTaskOutcome } from "@sema-agent/core";
2
+ /** The one write verb every outcome sink (TiDB/Pg ledger adapters + this File twin) exposes to main.ts,
3
+ * plus the OPTIONAL read aggregate backing `GET /v1/outcomes` (P2) — present on the SQL ledgers (their
4
+ * existing per-(signature, model) summary); absent here (the JSONL file IS the local dataset — a dashboard
5
+ * reads the file, the endpoint 501s honestly). */
2
6
  export interface OutcomeSink {
3
7
  recordCore(o: CoreTaskOutcome): Promise<void>;
4
8
  summary?(opts?: {
@@ -1,9 +1,18 @@
1
+ /**
2
+ * design/73 §1 consumption, LOCAL lane — the File twin of the TiDB/Pg outcome ledgers for the single-user /
3
+ * no-DB posture: append each core `TaskOutcome` fact VERBATIM to an owner-only JSONL ledger. Read-only v1
4
+ * (dashboard/backtest dataset — mirrors the SQL ledgers' read-only red line); no query surface here, the
5
+ * file IS the dataset. Non-load-bearing observability → no fsync (a crash may lose the tail fact; the run
6
+ * itself is unaffected — same posture as metrics).
7
+ */
1
8
  import { mkdirSync, appendFileSync } from "node:fs";
2
9
  import { join } from "node:path";
3
10
  export class FileOutcomeSink {
4
11
  path;
5
12
  constructor(root) {
6
13
  const dir = join(root, "outcomes");
14
+ // 0o700 dir + 0o600 file: outcome facts carry model/taskSignature/oracle detail — operator-only, parity
15
+ // with the other File ledgers' posture.
7
16
  mkdirSync(dir, { recursive: true, mode: 0o700 });
8
17
  this.path = join(dir, "outcomes.jsonl");
9
18
  }
@@ -1,19 +1,39 @@
1
1
  export declare class FileResumeAnchorStore {
2
2
  private readonly dir;
3
+ /** sessionId → (eventId → {entryId, owner}) — the same in-memory shape MemoryResumeAnchorStore holds, hydrated on ctor. */
3
4
  private readonly bySession;
5
+ /** sessionId → the open append log (one O_APPEND fd per session, lazily opened on first put, held for lifetime). */
4
6
  private readonly logs;
5
7
  constructor(root: string);
8
+ /** `<dir>/<sanitizePathComponent(sessionId)>.jsonl` — the per-session log path (sessionId is uuidv7, already safe). */
6
9
  private pathFor;
10
+ /** Replay every session log into the in-memory index on boot (torn-tail-safe via readJsonlRecords; last-wins upsert
11
+ * so a re-stamped eventId reflects the latest appended record). The in-memory index is keyed by the SANITIZED
12
+ * sessionId throughout (put/resolve/list/delete all sanitize), and each on-disk file's base name IS
13
+ * `sanitizePathComponent(sessionId)` — so the index key is just the file base, with no de-sanitize needed. */
7
14
  private hydrate;
15
+ /** Open (or reuse) the append log for a session. The file base is the SANITIZED id (matches hydrate's de-sanitize). */
8
16
  private logFor;
17
+ /** Record/overwrite the anchor for one settled message boundary (idempotent upsert; parity with the SQL
18
+ * ON DUPLICATE/CONFLICT). Appends a durable record (fsync:true) THEN upserts the index — a crash after the
19
+ * fsync is recovered by hydrate (the record is on disk); a crash before it just loses an un-acked anchor. */
9
20
  put(sessionId: string, eventId: string, entryId: string, owner: string | null): Promise<void>;
21
+ /** Resolve a shell-supplied eventId → the persisted SessionTreeEntry.id for TaskSpec.resumeAt; undefined = unknown
22
+ * anchor (the route 4xx's before handing core a bad id). owner null-safe guard = the SQL `owner <=> ?` defense-in-depth. */
10
23
  resolve(sessionId: string, eventId: string, owner: string | null): Promise<string | undefined>;
24
+ /** 2c session-sync: ALL anchors for a session (every eventId→entryId + its owner), UNFILTERED — a cross-backend
25
+ * session EXPORT bundles the whole anchor map so the importer can RE-KEY owner to the target principal (§9). The
26
+ * route already proved session ownership (the export gate), so no per-row owner filter here. Twin of the SQL stores. */
11
27
  listBySession(sessionId: string): Promise<Array<{
12
28
  eventId: string;
13
29
  entryId: string;
14
30
  owner: string | null;
15
31
  }>>;
32
+ /** E21 (§0.5 delete) — purge ALL anchors for a session, scoped by session_id ALONE (the route already proved session
33
+ * ownership; a per-row owner guard would orphan rows stored under a divergent per-run principal). Idempotent (count).
34
+ * Closes + unlinks the session log so the dir does not leak a file, and drops the in-memory index entry. */
16
35
  deleteBySession(sessionId: string): Promise<number>;
36
+ /** Release every open append-log fd (called by LocalBackend.close so a graceful restart can re-open the data dir). */
17
37
  dispose(): void;
18
38
  }
19
39
  //# sourceMappingURL=file-resume-anchor-store.d.ts.map
@@ -1,23 +1,47 @@
1
+ /**
2
+ * File-backed resume-at anchor map (E18) — the DURABLE local twin of {@link MemoryResumeAnchorStore}, persisting the
3
+ * service-owned eventId→entryId translation across a process restart (P0.5 variant-1, clay 2026-06-26; the file-backed
4
+ * follow-on the Memory twin's header promised). Behaviour is byte-identical to the Memory store (put/resolve/
5
+ * deleteBySession + the §0.5/2c `listBySession` add); the only difference is durability.
6
+ *
7
+ * Persistence: one append-only JSONL log per session at `<root>/resume-anchors/<sanitizePathComponent(sessionId)>.jsonl`,
8
+ * each line `{eventId, entryId, owner, ts}`. `put` APPENDS (fsync:true — an anchor is a durable rewind boundary; a lost
9
+ * anchor silently breaks the next rewind), and the in-memory index is upserted last-wins so a re-stamped eventId
10
+ * overwrites (parity with the SQL ON DUPLICATE/CONFLICT). On construction the index is HYDRATED by replaying every
11
+ * session log via core's `readJsonlRecords` (torn-tail-safe: a crash mid-append drops only the partial last line, and a
12
+ * later `put` re-records it). `resolve`/`listBySession` read the in-memory index (O(1)/O(per-session)); `deleteBySession`
13
+ * unlinks the file + drops the index entry. owner null-safe guard (`(a??null)===(b??null)`) = the SQL `owner <=> ?`
14
+ * defense-in-depth twin. Single-process (the FileStorageBackend boot lock guarantees one writer per data dir), so there
15
+ * is no per-op lock — the in-process model is the premise (core fs-atomic BootLock).
16
+ */
1
17
  import { join } from "node:path";
2
18
  import { existsSync, readdirSync, rmSync } from "node:fs";
3
19
  import { AppendLog, ensureDir, readJsonlRecords, sanitizePathComponent } from "@sema-agent/core";
20
+ /** Null-safe owner equality — the JS twin of the SQL `owner <=> ?` guard (a legacy null-owner anchor matches owner=null). */
4
21
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
5
22
  export class FileResumeAnchorStore {
6
23
  dir;
24
+ /** sessionId → (eventId → {entryId, owner}) — the same in-memory shape MemoryResumeAnchorStore holds, hydrated on ctor. */
7
25
  bySession = new Map();
26
+ /** sessionId → the open append log (one O_APPEND fd per session, lazily opened on first put, held for lifetime). */
8
27
  logs = new Map();
9
28
  constructor(root) {
10
29
  this.dir = join(root, "resume-anchors");
11
30
  ensureDir(this.dir);
12
31
  this.hydrate();
13
32
  }
33
+ /** `<dir>/<sanitizePathComponent(sessionId)>.jsonl` — the per-session log path (sessionId is uuidv7, already safe). */
14
34
  pathFor(sessionId) {
15
35
  return join(this.dir, `${sanitizePathComponent(sessionId)}.jsonl`);
16
36
  }
37
+ /** Replay every session log into the in-memory index on boot (torn-tail-safe via readJsonlRecords; last-wins upsert
38
+ * so a re-stamped eventId reflects the latest appended record). The in-memory index is keyed by the SANITIZED
39
+ * sessionId throughout (put/resolve/list/delete all sanitize), and each on-disk file's base name IS
40
+ * `sanitizePathComponent(sessionId)` — so the index key is just the file base, with no de-sanitize needed. */
17
41
  hydrate() {
18
42
  let names;
19
43
  try {
20
- names = readdirSync(this.dir);
44
+ names = readdirSync(this.dir); // a fresh root has the (just-ensured) empty dir → []
21
45
  }
22
46
  catch {
23
47
  return;
@@ -25,19 +49,20 @@ export class FileResumeAnchorStore {
25
49
  for (const name of names) {
26
50
  if (!name.endsWith(".jsonl"))
27
51
  continue;
28
- const key = name.slice(0, -".jsonl".length);
52
+ const key = name.slice(0, -".jsonl".length); // the file base IS sanitizePathComponent(sessionId) = the index key
29
53
  const records = readJsonlRecords(join(this.dir, name));
30
54
  if (!records.length)
31
55
  continue;
32
56
  const m = this.bySession.get(key) ?? new Map();
33
57
  for (const r of records) {
34
58
  if (!r || typeof r.eventId !== "string" || typeof r.entryId !== "string")
35
- continue;
36
- m.set(r.eventId, { entryId: r.entryId, owner: r.owner ?? null });
59
+ continue; // skip a malformed record
60
+ m.set(r.eventId, { entryId: r.entryId, owner: r.owner ?? null }); // last-wins upsert (replay order = append order)
37
61
  }
38
62
  this.bySession.set(key, m);
39
63
  }
40
64
  }
65
+ /** Open (or reuse) the append log for a session. The file base is the SANITIZED id (matches hydrate's de-sanitize). */
41
66
  logFor(sessionId) {
42
67
  const key = sanitizePathComponent(sessionId);
43
68
  let log = this.logs.get(key);
@@ -47,6 +72,9 @@ export class FileResumeAnchorStore {
47
72
  }
48
73
  return log;
49
74
  }
75
+ /** Record/overwrite the anchor for one settled message boundary (idempotent upsert; parity with the SQL
76
+ * ON DUPLICATE/CONFLICT). Appends a durable record (fsync:true) THEN upserts the index — a crash after the
77
+ * fsync is recovered by hydrate (the record is on disk); a crash before it just loses an un-acked anchor. */
50
78
  async put(sessionId, eventId, entryId, owner) {
51
79
  const key = sanitizePathComponent(sessionId);
52
80
  this.logFor(sessionId).append({ eventId, entryId, owner: owner ?? null, ts: new Date().toISOString() }, true);
@@ -57,10 +85,15 @@ export class FileResumeAnchorStore {
57
85
  }
58
86
  m.set(eventId, { entryId, owner });
59
87
  }
88
+ /** Resolve a shell-supplied eventId → the persisted SessionTreeEntry.id for TaskSpec.resumeAt; undefined = unknown
89
+ * anchor (the route 4xx's before handing core a bad id). owner null-safe guard = the SQL `owner <=> ?` defense-in-depth. */
60
90
  async resolve(sessionId, eventId, owner) {
61
91
  const e = this.bySession.get(sanitizePathComponent(sessionId))?.get(eventId);
62
92
  return e && ownerEq(e.owner, owner) ? e.entryId : undefined;
63
93
  }
94
+ /** 2c session-sync: ALL anchors for a session (every eventId→entryId + its owner), UNFILTERED — a cross-backend
95
+ * session EXPORT bundles the whole anchor map so the importer can RE-KEY owner to the target principal (§9). The
96
+ * route already proved session ownership (the export gate), so no per-row owner filter here. Twin of the SQL stores. */
64
97
  async listBySession(sessionId) {
65
98
  const m = this.bySession.get(sanitizePathComponent(sessionId));
66
99
  if (!m)
@@ -70,6 +103,9 @@ export class FileResumeAnchorStore {
70
103
  out.push({ eventId, entryId: e.entryId, owner: e.owner });
71
104
  return out;
72
105
  }
106
+ /** E21 (§0.5 delete) — purge ALL anchors for a session, scoped by session_id ALONE (the route already proved session
107
+ * ownership; a per-row owner guard would orphan rows stored under a divergent per-run principal). Idempotent (count).
108
+ * Closes + unlinks the session log so the dir does not leak a file, and drops the in-memory index entry. */
73
109
  async deleteBySession(sessionId) {
74
110
  const key = sanitizePathComponent(sessionId);
75
111
  const removed = this.bySession.get(key)?.size ?? 0;
@@ -85,16 +121,19 @@ export class FileResumeAnchorStore {
85
121
  rmSync(path, { force: true });
86
122
  }
87
123
  catch {
124
+ /* best-effort — a stale file is harmless (the index entry is already gone) */
88
125
  }
89
126
  }
90
127
  return removed;
91
128
  }
129
+ /** Release every open append-log fd (called by LocalBackend.close so a graceful restart can re-open the data dir). */
92
130
  dispose() {
93
131
  for (const log of this.logs.values()) {
94
132
  try {
95
133
  log.close();
96
134
  }
97
135
  catch {
136
+ /* best-effort */
98
137
  }
99
138
  }
100
139
  this.logs.clear();