@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
@@ -6,18 +6,31 @@ import { exportSession, exportSessionManifest, importSession, classifySyncRelati
6
6
  import { isUuidV7, isUuidShape } from "../../security.js";
7
7
  import { sendJson, sendError, msg } from "../send.js";
8
8
  import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
9
+ /** 2c session-sync PUT /sync/blobs/:hash per-blob upload cap. A snapshot blob is ONE file's bytes, bounded by core's
10
+ * DEFAULT_SNAPSHOT_BOUNDS (the whole tree ≤ 64 MiB), so a single blob can never exceed the per-snapshot total — this
11
+ * cap (= 64 MiB, ≥ that bound) is the reject-not-OOM ceiling: the blob body is read with a running size guard that
12
+ * 413s the instant it is exceeded (NEVER buffered unboundedly), the §5 "reject, not OOM" requirement. Larger than
13
+ * MAX_BODY (8 MiB, the JSON cap) on purpose — a blob is binary file content, not a JSON request. */
9
14
  const SYNC_BLOB_MAX_BYTES = 64 * 1024 * 1024;
15
+ /** 2c session-sync P1d-β: the per-LINE cap on the Phase-B NDJSON entry stream — ONE entry must fit in this (the §1.5
16
+ * ruling: a session_event row is ≤~20KB because core offloads >20K tool results, so a few MiB is a generous ceiling).
17
+ * A line exceeding it → 413 (reject, not OOM): a single oversized entry would otherwise force an unbounded line buffer. */
10
18
  const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
19
+ /** 2c session-sync P1d-β: the Phase-B entry batch size — entries are fed to `appendBatch` in chunks of this many, so
20
+ * in-flight memory is O(batch), not O(session). 500 mirrors the PULL stream's keyset page size (one bounded txn). */
11
21
  const SYNC_IMPORT_BATCH = 500;
22
+ /** 64 lowercase-hex chars = a sha256 content-address (the blob hash + the GET :hash param shape). */
12
23
  const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
13
24
  export function createSessionSyncLocal() {
14
25
  const pendingStagings = new Map();
15
26
  const importLeases = new Map();
27
+ /** 租约续期 —— entries 上传腿每收一批就调,所以大会话的长上传不会被误判成陈旧。 */
16
28
  const touchImportLease = (sessionId) => {
17
29
  const cur = importLeases.get(sessionId);
18
30
  if (cur)
19
31
  importLeases.set(sessionId, { ...cur, at: Date.now() });
20
32
  };
33
+ /** Drop a finished/aborted staging from the in-process maps + release its per-session import lease. Idempotent. */
21
34
  const cleanupStaging = (stagingId) => {
22
35
  const p = pendingStagings.get(stagingId);
23
36
  if (p)
@@ -31,13 +44,25 @@ export async function handleSessionSync(req, res, url, ctx) {
31
44
  await handleSessionSyncBody(req, res, url, ctx, miss);
32
45
  return !miss.fell;
33
46
  }
47
+ /** 路由体 = 从 `server.ts` 的 `handle()` 里**整段剪切**过来的原文(唯一改动:统一去缩进两格)。 */
34
48
  async function handleSessionSyncBody(req, res, url, ctx, miss) {
35
49
  const { deps } = ctx;
36
50
  const { readJson, readRawBody, safeDecode, sessionOwnerScopeForWrite } = ctx.helpers;
37
51
  const { pendingStagings, importLeases, touchImportLease, cleanupStaging } = ctx.local.sessionSync;
52
+ // ── 2c session-sync (P1d) — the cloud as a SYNC PEER (docs/DESIGN-session-sync.md §15 + §5–§10) ──────────────
53
+ // The real 2c topology is TWO PROCESSES (the local shell's local backend + this cloud service); these routes let
54
+ // the local peer PULL a session's whole state out of the cloud or PUSH its own in. exportSession/importSession
55
+ // (src/session-sync.ts) fan out across ALL four session subsystems (entries + fileSnapshot + sessionPolicy +
56
+ // resumeAnchor) — they need the WHOLE backend (deps.backend), not the individual stores. Every route is
57
+ // owner-gated like fork/delete (§9): a PULL 404s a non-owner (no exfiltration oracle); a PUSH re-stamps the
58
+ // imported owner to the AUTHENTICATED principal (the bundle's owner is ignored). 501 when no durable session
59
+ // store is wired (parity with fork: capability says yes ⟺ the route resolves).
38
60
  {
39
61
  const sm = /^\/v1\/sessions\/([^/]+)\/sync\/(manifest|entries|import|plan|blobs(?:\/([^/]+))?|snapshots\/([^/]+)\/blobs\/([^/]+)|import\/([^/]+)\/entries)$/.exec(url);
40
62
  if (sm) {
63
+ // Durable-store gate (501) FIRST is wrong for an auth oracle, so AUTH-FIRST below mirrors fork/delete; but the
64
+ // backend is needed even to know the owner, so we gate on it up front and rely on AUTH being checked before
65
+ // any store touch. ownerOf is the existence/authz primitive every branch uses.
41
66
  const ownerOf = deps.sessionStorage?.ownerOf?.bind(deps.sessionStorage);
42
67
  if (!deps.backend || !ownerOf) {
43
68
  sendError(res, 501, "capability.session_store_required", "session sync requires a durable session store");
@@ -47,32 +72,44 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
47
72
  if (!scope.fleetWide && scope.gateOwner === null) {
48
73
  sendError(res, 401, "auth.unauthorized", "unauthorized");
49
74
  return;
50
- }
75
+ } // auth-first
51
76
  const sessionId = safeDecode(sm[1]);
77
+ // uuid ANY version — a v4-keyed legacy shell session must be pushable (it was
78
+ // creatable via the lenient submit face); crafted-id classes (LIKE metachars, over-long) still shut out.
52
79
  if (sessionId === null || !isUuidShape(sessionId)) {
53
80
  sendError(res, 400, "request.id_invalid", "invalid session id (must be a canonical uuid)");
54
81
  return;
55
82
  }
83
+ // Two owner gates, BOTH 404 a foreign session with no existence oracle:
84
+ // • PULL (read the cloud's session): the session MUST exist (undefined⇒404) AND the caller must own it.
85
+ // • PUSH/PLAN (write into / plan against the cloud): own-or-FRESH — `undefined` (no such session yet, the
86
+ // caller is about to import it) OR the caller's own ⇒ allowed; a DIFFERENT owner ⇒ 404.
56
87
  const owner = await ownerOf(sessionId);
57
88
  const ownsOrFleet = (o) => scope.fleetWide || o === scope.gateOwner;
58
89
  const pullGateOk = () => {
59
90
  if (owner === undefined) {
60
91
  sendError(res, 404, "not_found.session", "session not found");
61
92
  return false;
62
- }
93
+ } // must exist to pull
63
94
  if (!ownsOrFleet(owner)) {
64
95
  sendError(res, 404, "not_found.session", "session not found");
65
96
  return false;
66
- }
97
+ } // no oracle
67
98
  return true;
68
99
  };
69
100
  const pushGateOk = () => {
70
101
  if (owner !== undefined && !ownsOrFleet(owner)) {
71
102
  sendError(res, 404, "not_found.session", "session not found");
72
103
  return false;
73
- }
74
- return true;
104
+ } // foreign → no oracle
105
+ return true; // undefined (fresh, caller will import) OR own/fleet
75
106
  };
107
+ // GET …/sync/manifest — PULL: export the MANIFEST (entry IDS + per-snapshot relPath→blobHash + policy +
108
+ // anchors + leafId), NO entry payloads and NO blob bytes (P1d-α §1.5/§5/§15: bounded memory — the entries
109
+ // stream separately via …/sync/entries). The local peer diffs entryIds → classifySyncRelationshipByIds, then
110
+ // pulls the entries (NDJSON) + only the blobHashes it lacks. exportSessionManifest returns null ONLY for "no
111
+ // such session" (the owner gate above already proved existence+ownership, so a null here is a raced delete →
112
+ // 404).
76
113
  if (sm[2] === "manifest" && req.method === "GET") {
77
114
  if (!pullGateOk())
78
115
  return;
@@ -80,13 +117,22 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
80
117
  if (out === null) {
81
118
  sendError(res, 404, "not_found.session", "session not found");
82
119
  return;
83
- }
120
+ } // raced delete after the gate
84
121
  sendJson(res, 200, { manifest: out.manifest });
85
122
  return;
86
123
  }
124
+ // GET …/sync/entries[?afterSeq=N] — PULL the entry PAYLOADS as an NDJSON stream (P1d-α §1.5/§5/§15). The store
125
+ // seam keyset-PAGES the durable log (one pooled query per batch → bounded server memory; a slow client can't
126
+ // pin a connection), and we apply HTTP backpressure (await drain on a full socket buffer) so a slow consumer
127
+ // can't OOM the server either. The frame: a {"__sync":"begin",…} header line, then one JSON entry per line,
128
+ // then a {"__sync":"end","count":N} TRAILER line — a MISSING trailer signals a truncated stream to the client.
87
129
  if (sm[2] === "entries" && req.method === "GET") {
88
130
  if (!pullGateOk())
89
131
  return;
132
+ // afterSeq (optional resume cursor): a non-negative integer; reject anything else as a 400 (never a NaN
133
+ // cursor). It is the BACKEND-NATIVE durable `seq` cursor (yield entries whose seq > afterSeq) — a resume is
134
+ // always against the SAME backend, so the seq base (durable `seq` column vs. local 0-based index) needn't be
135
+ // portable across backends.
90
136
  const afterSeqRaw = new URL(req.url ?? "", "http://x").searchParams.get("afterSeq");
91
137
  let afterSeq;
92
138
  if (afterSeqRaw !== null) {
@@ -101,13 +147,16 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
101
147
  if (!stream) {
102
148
  sendError(res, 501, "capability.session_store_required", "session entry streaming requires a durable session store");
103
149
  return;
104
- }
150
+ } // method absent
105
151
  const it = await stream(sessionId, afterSeq === undefined ? undefined : { afterSeq });
106
152
  if (it === null) {
107
153
  sendError(res, 404, "not_found.session", "session not found");
108
154
  return;
109
- }
155
+ } // raced delete after the gate
156
+ // Headers committed — past this point we cannot change the status; a mid-stream error just ends the body
157
+ // WITHOUT the trailer, which the client reads as truncation.
110
158
  res.writeHead(200, { "content-type": "application/x-ndjson", "transfer-encoding": "chunked" });
159
+ // backpressure-aware write: if the socket buffer is full (write returns false), await 'drain' before the next.
111
160
  const write = async (line) => { if (!res.write(line))
112
161
  await once(res, "drain"); };
113
162
  let count = 0;
@@ -117,15 +166,21 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
117
166
  await write(`${JSON.stringify(entry)}\n`);
118
167
  count++;
119
168
  }
120
- await write(`${JSON.stringify({ __sync: "end", count })}\n`);
169
+ await write(`${JSON.stringify({ __sync: "end", count })}\n`); // trailer — its ABSENCE = truncated stream
121
170
  res.end();
122
171
  }
123
172
  catch (e) {
173
+ // The iterable (or a write) threw mid-stream. Headers are already sent, so we can't 4xx/5xx — just END the
174
+ // body. The MISSING trailer tells the client the stream was truncated. Log it for the operator.
124
175
  deps.logger?.error?.("sync entries stream failed", { sessionId, count, error: msg(e) });
125
176
  res.end();
126
177
  }
127
178
  return;
128
179
  }
180
+ // GET …/sync/snapshots/:key/blobs/:hash — PULL one content-addressed blob's RAW bytes. The SCOPE CHECK is the
181
+ // security boundary: a content-addressed blob is shared across sessions, so we must NOT let an owner fetch an
182
+ // arbitrary hash (another tenant's file content). We require the requested :hash to be a value in THIS
183
+ // session's snapshot `:key` manifest — a session owner may only fetch blobs ITS OWN snapshots reference.
129
184
  if (sm[4] !== undefined && req.method === "GET") {
130
185
  if (!pullGateOk())
131
186
  return;
@@ -141,6 +196,8 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
141
196
  }
142
197
  const fs = deps.fileSnapshotStore;
143
198
  const manifest = await fs?.exportManifest?.(sessionId, key);
199
+ // null manifest (no such snapshot) OR the hash is not referenced by it ⇒ 404 (the scope boundary — never a
200
+ // blind content-addressed read of an unscoped hash).
144
201
  if (!manifest || ![...manifest.values()].includes(hash)) {
145
202
  sendError(res, 404, "not_found.blob", "blob not found");
146
203
  return;
@@ -154,6 +211,10 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
154
211
  res.end(Buffer.from(bytes));
155
212
  return;
156
213
  }
214
+ // PUT …/sync/blobs/:hash — PUSH one content-addressed blob INTO the cloud's snapshot store, ahead of the bundle
215
+ // import (§5 streamed negotiation: the local peer GETs the manifest, then PUTs only the blobs the cloud lacks).
216
+ // own-or-FRESH gate. Verify sha256(body)===:hash (the content-address is the integrity guarantee; a transfer
217
+ // crosses the net) BEFORE storing. The body is read with a PER-BLOB cap (reject-not-OOM).
157
218
  if (sm[3] !== undefined && req.method === "PUT") {
158
219
  if (!pushGateOk())
159
220
  return;
@@ -167,8 +228,12 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
167
228
  sendError(res, 501, "capability.snapshot_store_required", "blob upload requires a durable snapshot store");
168
229
  return;
169
230
  }
170
- const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES);
231
+ const body = await readRawBody(req, SYNC_BLOB_MAX_BYTES); // 413s mid-stream if it exceeds the cap (no unbounded buffer)
232
+ // (a): SQL-default blob store cap — 413 typed, NOT the 502 the store's never-throw result would become
233
+ // (502 = "retry"; an over-cap blob can never land, so retrying it forever is the wrong contract).
171
234
  if (deps.snapshotBlobSqlCapBytes !== undefined && body.byteLength > deps.snapshotBlobSqlCapBytes) {
235
+ // 复审 D-F1/B-F2:归一漏网(多行体,旧门正则扫不到)——A6 迁移到 sendError 后此站点由
236
+ // errorCode 必填位置参数保结构(不再靠源级反射门兜底)。
172
237
  sendError(res, 413, "blob_too_large_for_sql", `blob of ${body.byteLength} bytes exceeds this deployment's SQL snapshot-blob cap (${deps.snapshotBlobSqlCapBytes} bytes — TiDB's txn-entry-size-limit is the low wall at 6MiB by default; the mysql-protocol packet limit sits above it) — configure MinIO object storage (MINIO_ENDPOINT/MINIO_ACCESS_KEY/MINIO_SECRET_KEY) for large snapshot blobs, or raise SNAPSHOT_BLOB_SQL_MAX_BYTES if your deployment lifted those limits`, { code: "blob_too_large_for_sql" });
173
238
  return;
174
239
  }
@@ -176,17 +241,29 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
176
241
  sendError(res, 400, "request.hash_mismatch", "blob hash mismatch");
177
242
  return;
178
243
  }
244
+ // core 1.142.0: putBlob is a fail-closed never-throw seam — a backend write error (e.g. a MinIO non-2xx PUT)
245
+ // is {ok:false}, NOT a thrown 500. Surface it as 502 so the local peer retries rather than treating the bundle
246
+ // blob as durably stored when it isn't.
179
247
  const put = await fs.putBlob(hash, new Uint8Array(body));
180
248
  if (!put.ok) {
181
249
  sendError(res, 502, put.error.code, "blob store write failed", { code: put.error.code });
182
250
  return;
183
251
  }
184
- res.writeHead(204).end();
252
+ res.writeHead(204).end(); // no body
185
253
  return;
186
254
  }
255
+ // POST …/sync/import — PHASE A (PUSH, P1d-β §15.2): a SMALL JSON body — `{entryIds, snapshots, policy, anchors,
256
+ // resolution?}` — NO entries (they stream in Phase B). Classify (§7), pre-check blob presence (§10), guard the
257
+ // §4 import-vs-import lease + the active-run window, then mint a stagingId, open a StagingHandle, and park the
258
+ // Phase-A metadata. Returns `200 {stagingId, relation}`. The entries-inline whole-bundle import is GONE (the
259
+ // §1.5 OOM offender) — Phase B streams the log into the shadow id so server memory is bounded by batch.
187
260
  if (sm[2] === "import" && req.method === "POST") {
188
261
  if (!pushGateOk())
189
262
  return;
263
+ // sessionMirror 观测面(裁定 (c),论证在 ServiceDeps.sessionMirrorRuling 注释):Phase A
264
+ // 到达 = 一次镜像 push 落到本 server 的时点信号。该 principal 若处 org 治理态(required=true),记
265
+ // audit 日志 + metric,把「治理生效」与「到达流」在运营面对上——这是 server 侧唯一诚实可观测的面
266
+ // (没到达的会话 server 看不见,真执法在壳)。fire-and-forget + 双层吞错:观测绝不拖慢/拖垮 sync。
190
267
  if (deps.sessionMirrorRuling && !scope.fleetWide && scope.gateOwner !== null) {
191
268
  const auditPrincipal = scope.gateOwner;
192
269
  void deps.sessionMirrorRuling(auditPrincipal).then((mirror) => {
@@ -208,6 +285,8 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
208
285
  sendError(res, 400, "request.invalid_json", "invalid JSON body");
209
286
  return;
210
287
  }
288
+ // Shape validation (§10): entryIds must be a string[]; snapshots an array of {key:string, manifest:array};
289
+ // policy/anchors arrays. A malformed shape → 400 (never a 500 from a downstream TypeError).
211
290
  if (!Array.isArray(body.entryIds) || !body.entryIds.every((x) => typeof x === "string")) {
212
291
  sendError(res, 400, "request.field_invalid", "entryIds must be an array of strings");
213
292
  return;
@@ -222,19 +301,56 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
222
301
  return;
223
302
  }
224
303
  const resolution = body.resolution;
304
+ // 🔴 方案 B(黑板 [1701]→[1702]→[1707] 三端对齐):**可选** `logDigest` = 源端整条日志的内容摘要
305
+ // (core 1.414 `sessionLogDigest`)。老客户端不送 ⇒ 行为与此前逐字相同。坏形**不拒**,当作没送
306
+ // (core 口径:摘要不符/不可比永远不得变成 error / 409 / 拒绝同步 —— 它只意味着"多干一趟活")。
225
307
  const srcLogDigest = typeof body.logDigest === "string" && body.logDigest.length > 0 ? body.logDigest : undefined;
226
308
  const entryIds = body.entryIds;
227
309
  const snapshots = body.snapshots;
228
310
  const policy = body.policy;
229
311
  const anchors = body.anchors;
312
+ // §7 classify (BEFORE issuing a stagingId): dst current ids → classifySyncRelationshipByIds. A fork/stale
313
+ // without overwrite-dst → 409 (no stream needed). `identical` → 200 {relation:"identical"} (no Phase B; the
314
+ // snapshot/policy/anchor replay is idempotent and not strictly needed — the dst already holds this log; the
315
+ // shell may re-push with no consequence). NB the AUTHORITATIVE re-classify is in commit() (the TOCTOU close).
230
316
  const dstIds = (await deps.sessionStorage.listEntryIds?.(sessionId)) ?? null;
231
317
  const relA = classifySyncRelationshipByIds(entryIds, dstIds);
232
318
  if ((relA.relation === "fork" || relA.relation === "stale") && resolution !== "overwrite-dst") {
233
319
  sendError(res, 409, "conflict", "sync conflict", { relation: relA });
234
320
  return;
235
321
  }
322
+ // 🔴 内容判据(方案 B 的 Phase A 半场):`identical` 只说明 **id 集合**相等。源端内容在 Phase A 还没上传,
323
+ // 所以这里只能靠调用方送来的 `logDigest`;拿目的端自己的日志算一次摘要跟它比。
324
+ // ⚠️ 目的端全量读**只在这条路上做**(要短路、且调用方送了摘要)—— 成本只花在它能改变结论的地方。
325
+ // 三态:相等 ⇒ 可以短路;不等 ⇒ **不短路**(照常开 staging 走完整同步);判不可比(scheme 不同/
326
+ // 对端没送)⇒ 同样**不短路**,退回完整同步 = 安全方向。
236
327
  let payloadVerified = false;
237
328
  let digestSaysDiffer = false;
329
+ // ── 这条路依赖的那条**性质**,以及它为什么由依赖下限 + 一条行为测试守着(不是由版本名判断守着)──
330
+ // 本路要求:**同一份日志,内存形与 JSON 往返形必须同摘要**。因为它比的正是「客户端从**内存**算的
331
+ // `logDigest`」对「server 从**已持久化**(JSON 往返过)的日志算的摘要」。
332
+ // `sessionLogDigest` 的 **v1** 不满足它:v1 走引擎那个规范化序列化器,**刻意**区分 `{a: undefined}`
333
+ // 与 `{}`,而 core 自己成规模产 own-key=undefined 的 entry(清除标签、无 data 的 custom entry)
334
+ // ⇒ 两形必然不等 ⇒ **永不短路**,却看起来像在工作(症状只是「Phase B 总在跑」,查不出原因):
335
+ // 纯成本、零收益,还多一次目的端全量读。core 黑板 [1709] 明确「别接 v1」,1.415.0 改算法并升 scheme v1→v2。
336
+ //
337
+ // 🔴 我一开始写的是 `SESSION_LOG_DIGEST_SCHEME !== "sema-log-v1"` 这道**名字门**。升上 1.415 后
338
+ // `tsc` 直接判它恒真(core 把 scheme 收成了字面量类型)—— 而这提醒了一件更要紧的事:名字门本来就
339
+ // **守不住这条性质**。它只认识"v1 这个名字",对将来任何一个同样破坏往返稳定性的 v3 一样放行。
340
+ // ⇒ 改成两道**真守得住**的:
341
+ // ① **依赖下限** `@sema-agent/core: ^1.416.0`。⚠️ package.json 是 JSON、写不了旁注,所以"这条下限是
342
+ // 承重的、不是随手升的"记在两处:**本注释** + `CHANGELOG.md` 1.277.0/1.277.1 两条。
343
+ // 降回更低的下限,tsc 不会有任何抱怨 —— 拦它的是 ② 那两道行为门。
344
+ // ② **行为门**(都在 `test/sync-content-digest.test.ts`),两条**方向相反**、缺一不可:
345
+ // · **往返稳定**:同一份含 own-key=undefined 的日志,内存形与 JSON 往返形必须同摘要。
346
+ // 它坏 ⇒ **永不短路**(纯成本,安全方向)。core 摘要 v1 不满足它(v1 刻意区分 `{a:undefined}`
347
+ // 与 `{}`,而 core 成规模产这种 entry),黑板 [1709] 明确「别接 v1」。
348
+ // · 🔴 **防撞**:内容不同 ⇒ 摘要必须不同,**按深度枚举**(坏值与真实内容同级)。
349
+ // 它坏 ⇒ **错误短路** = 静默丢数据,方向与上面相反、后果严重得多。core 摘要 **v2** 不满足它
350
+ // (逐键降级只做在 entry 顶层,而真实内容在深度 ≥2 ⇒ 一个 bigint 吃掉同级全部内容),
351
+ // 1.416.0 / v3 修复(黑板 [1714])。
352
+ // 两条钉的都是**性质**、不是版本名 —— 任何 scheme 再破坏其一都会红。
353
+ // (教训同形于 core 的 RB-149:源码/名字级判据挡的是写法,不是行为。)
238
354
  if (relA.relation === "identical" && srcLogDigest !== undefined) {
239
355
  const dstEntries = await deps.sessionStorage.exportEntries?.(sessionId).catch(() => null) ?? null;
240
356
  const dstDigest = dstEntries === null ? undefined : sessionLogDigest(dstEntries);
@@ -244,10 +360,16 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
244
360
  else
245
361
  digestSaysDiffer = true;
246
362
  }
363
+ // 不可比 ⇒ 两个标志都保持 false ⇒ 走下面「不短路」那一支(退回完整同步)
247
364
  if (!payloadVerified)
248
365
  digestSaysDiffer = true;
249
366
  }
250
367
  if (relA.relation === "identical" && !digestSaysDiffer) {
368
+ // 对抗复查 A-2(CRITICAL):identical 不能裸短路——若上一轮 commit 后 snapshot/policy/anchor 重放
369
+ // 失败(422),entries 已落库,重试的 Phase A 就落在这里;裸 200 会把「对话已同步但文件树缺失」
370
+ // 焊死成永久分裂(Phase B 的重放永远不再运行)。identical 时把附随物幂等重放(snapshot=
371
+ // content-addressed importManifest,已存在=no-op;policy/anchor put=幂等),失败=同款 422,
372
+ // 客户端 re-PUT blob 再重试——自愈闭环。
251
373
  const fss = deps.fileSnapshotStore;
252
374
  if (fss?.importManifest && fss.getBlob && snapshots.length > 0) {
253
375
  const getBlobA = (h) => fss.getBlob(h);
@@ -267,13 +389,23 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
267
389
  if (deps.resumeAnchorStore)
268
390
  for (const a of anchors)
269
391
  await deps.resumeAnchorStore.put(sessionId, a.eventId, a.entryId, importingP);
392
+ // 🔴 措辞诚实化(2026-07-25,深挖第一轮 B2/B3/B4/G1):`identical` 的判据是 **entry id 集合相等**,
393
+ // 分类器(`classifySyncRelationshipByIds`)**从不看载荷/父子结构** —— 而 entry id 是 uuidv7、
394
+ // **不是内容寻址**,所以「同 id 异载荷」结构上完全可能。此时 entries 被**整段跳过**,客户端拿到 200,
395
+ // 目的端却仍是它自己那份内容(探针用真 HTTP 复现过:本地改过内容再 push,云端保持原样、200)。
396
+ // 现在把判据写进响应(`basis`)并显式声明**未验证载荷**,这样客户端不会把它读成"内容已同步"。
397
+ // ⚠️ 这只是止谎,**不是修**:真修要 Phase A 能拿到内容摘要(Phase A 只收 entryIds,载荷在 Phase B),
398
+ // 那是跨仓 wire 契约改动,已带证据上黑板与 cli/core 对齐。别把 `basis` 当成问题已解决。
270
399
  sendJson(res, 200, {
271
400
  relation: "identical",
401
+ // 判据如实:调用方送了可比摘要且相等 ⇒ 内容**真的**验过;否则仍只是 id 集合相等。
272
402
  basis: payloadVerified ? "entry-ids+digest" : "entry-ids",
273
403
  payloadVerified,
274
404
  });
275
405
  return;
276
406
  }
407
+ // §4 active-run guard (don't drop a live append) + per-session import lease (don't let two staged imports race
408
+ // to commit the same session). Both 409.
277
409
  const activeTaskId = await deps.runStore?.getActiveTaskId(sessionId);
278
410
  if (activeTaskId) {
279
411
  sendError(res, 409, "session_active", "session has an active run; sync after it settles", { activeTaskId });
@@ -281,22 +413,34 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
281
413
  }
282
414
  const heldLease = importLeases.get(sessionId);
283
415
  if (heldLease) {
416
+ // 🔴 数值守卫而不是直接乘:任何**早于这个字段**的 config 对象(旧部署配置、精简的测试 harness)
417
+ // 会让它是 `undefined` ⇒ `undefined * 1000 = NaN` ⇒ `NaN > 0` 为假 ⇒ **接管静默不发生**,
418
+ // 自愈通道在不报错的情况下消失。实测撞到过(一份最小 config 的探针 harness)。缺省取与 config
419
+ // 同一个默认值(600s),而不是 0 —— 0 的语义是"明确关掉",不该由"漏配"来选中它。
284
420
  const staleSecRaw = deps.config.syncImportLeaseStaleSec;
285
421
  const staleSec = typeof staleSecRaw === "number" && Number.isFinite(staleSecRaw) ? staleSecRaw : 600;
286
422
  const staleMs = staleSec * 1000;
287
423
  const ageMs = Date.now() - heldLease.at;
288
424
  if (staleMs > 0 && ageMs >= staleMs) {
425
+ // 接管:上一个持有者已经 staleMs 没有任何活动(既没上传 entries 也没 commit)⇒ 判它死了。
426
+ // 先清掉它的 staging(否则那份 staged 行会留成孤儿),再让本次 Phase A 正常继续。
289
427
  deps.logger?.warn?.("sync_import_lease_taken_over", { sessionId, ageMs, staleMs, stagingId: heldLease.stagingId });
290
428
  cleanupStaging(heldLease.stagingId);
291
- importLeases.delete(sessionId);
429
+ importLeases.delete(sessionId); // cleanupStaging 只在 pendingStagings 里还有它时才删租约,这里兜底
292
430
  }
293
431
  else {
432
+ // 错误文案点明**会自愈**:此前的措辞让客户端以为是永久状态,而它其实等一会儿就能重试。
294
433
  const retryAfterSec = staleMs > 0 ? Math.max(1, Math.ceil((staleMs - ageMs) / 1000)) : undefined;
295
434
  sendError(res, 409, "import_in_flight", "an import is already in flight for this session"
296
435
  + (retryAfterSec !== undefined ? ` (a stalled holder is taken over after ${staleSec}s of inactivity; retry in ~${retryAfterSec}s)` : ""), retryAfterSec !== undefined ? { retryAfterSec } : undefined);
297
436
  return;
298
437
  }
299
438
  }
439
+ // §10 blob-presence pre-check (BEFORE staging): every referenced blobHash must be present in the cloud's OWN
440
+ // snapshot store (blobs were pre-PUT). A missing one → 422 (PUT it, then retry) — fail FAST so we never open a
441
+ // staging that can only fail at commit. Uses the store's hasBlobs (a single SQL INDEX probe — finding 12) NOT
442
+ // a full getBlob GET-per-hash (under MinIO that would download + re-hash every ≤64 MiB object just to test
443
+ // presence). Feature-detect hasBlobs; if a store lacks it, fall back to getBlob (parity for older stores).
300
444
  const fss = deps.fileSnapshotStore;
301
445
  const canImportSnapshots = typeof fss?.importManifest === "function";
302
446
  if (canImportSnapshots) {
@@ -322,8 +466,15 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
322
466
  }
323
467
  }
324
468
  }
469
+ // importingPrincipal (§9) + operatorOk (E6 tighten-only) — mirror the v1 fixes; applied at commit (policy replay).
325
470
  const importingPrincipal = scope.fleetWide ? (owner ?? null) : scope.gateOwner;
326
471
  const operatorOk = explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals);
472
+ // Mint the stagingId TOKEN (the route supplies it, NOT the store) + open the handle + take the lease + park
473
+ // the metadata. Phase B drives it; abandoned ones are reaped (durable rows) / dropped on restart. The token is
474
+ // a SHORT random hex (8 bytes = 16 chars), NOT a full uuidv7: the staging id is `${sessionId}#stg-${token}` and
475
+ // session_event.session_id is VARCHAR(64) — a 36-char uuidv7 session id + "#stg-" (5) leaves 23 chars, so a
476
+ // 16-char token fits (and is collision-resistant for the few in-flight stagings of ONE session — the lease
477
+ // already serializes them per session). randomBytes is unguessable (it rides the URL the client gets back).
327
478
  const token = randomBytes(8).toString("hex");
328
479
  const handle = deps.sessionStorage.beginImportStaging(sessionId, token);
329
480
  const stagingId = handle.stagingId;
@@ -335,6 +486,11 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
335
486
  sendJson(res, 200, { stagingId, relation: relA.relation });
336
487
  return;
337
488
  }
489
+ // POST …/sync/import/:stagingId/entries — PHASE B (PUSH, P1d-β §15.3): stream the entry log as an NDJSON request
490
+ // body into the staged shadow id (bounded memory — line buffer + batch), driving core's STREAMING import gate
491
+ // ({@link StreamingImportValidator}) ONE entry per line, then `finish()` at stream end (exactly-one-root +
492
+ // leaf-resolvable) → commit (the atomic swap) → replay snapshots/policy/anchors. Re-POSTing the same stagingId is
493
+ // idempotent (appendBatch dedups; the commit re-runs). sm[6] = the stagingId.
338
494
  if (sm[6] !== undefined && req.method === "POST") {
339
495
  const stagingId = safeDecode(sm[6]);
340
496
  if (stagingId === null) {
@@ -342,6 +498,8 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
342
498
  return;
343
499
  }
344
500
  const pending = pendingStagings.get(stagingId);
501
+ // A wrong/foreign/already-finished stagingId → 404 (no oracle). It must also belong to THIS url's session and
502
+ // to the caller (the principal that opened it), unless the caller is fleet-wide (ops on-behalf).
345
503
  if (!pending || pending.realSessionId !== sessionId) {
346
504
  sendError(res, 404, "not_found.staging", "staging not found");
347
505
  return;
@@ -350,15 +508,24 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
350
508
  sendError(res, 404, "not_found.staging", "staging not found");
351
509
  return;
352
510
  }
353
- const getBlob = (h) => deps.fileSnapshotStore.getBlob(h);
511
+ const getBlob = (h) => deps.fileSnapshotStore.getBlob(h); // cloud's OWN store
354
512
  const canImportSnapshots = typeof deps.fileSnapshotStore?.importManifest === "function";
513
+ // ── Stream the NDJSON request body → core's StreamingImportValidator per line → batched appendBatch (bounded
514
+ // memory: line buffer + batch). The streaming validator IS the single invariant gate (unique ids /
515
+ // parent-before-child / structural refs / one-root / leaf-resolvable) — core 1.143.0 — so the service
516
+ // no longer re-states ANY of those rules (no ad-hoc Tier-1 pre-check, no Tier-2 read-back-and-re-validate;
517
+ // the invariant gate belongs to core and the service does not duplicate it). `step()` throws SessionError("invalid_session") on a per-entry violation;
518
+ // `finish()` throws on the end-of-stream invariants. The route keeps ONLY the TRANSPORT-level guards (the
519
+ // per-line byte cap → 413, JSON parse → 400, and the object/string-id shape so a `null`/non-object line maps
520
+ // to a typed 422 instead of an unmapped TypeError); these are NOT session-tree invariants. ──
355
521
  const validator = new StreamingImportValidator();
356
- let lineBuf = "";
522
+ let lineBuf = ""; // the partial-line accumulator (bytes counted against the per-line cap)
357
523
  let pendingBatch = [];
358
524
  const flush = async () => { if (pendingBatch.length) {
359
525
  await pending.handle.appendBatch(pendingBatch);
360
526
  pendingBatch = [];
361
527
  } };
528
+ // Map a transport-level violation to a typed 4xx WITHOUT committing (the handle's staged rows are abandoned → reaped).
362
529
  class PrecheckError extends Error {
363
530
  status;
364
531
  constructor(status, msg) {
@@ -366,16 +533,21 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
366
533
  this.status = status;
367
534
  }
368
535
  }
369
- const streamedIds = new Set();
536
+ const streamedIds = new Set(); // A-3: bind the actual stream to Phase A's declaration
370
537
  const onEntry = (e) => {
371
538
  if (!e || typeof e !== "object" || typeof e.id !== "string")
372
539
  throw new PrecheckError(422, "malformed entry (missing id)");
373
540
  streamedIds.add(e.id);
374
- validator.step(e);
541
+ validator.step(e); // core's invariant gate, one entry at a time (oldest-first) — throws SessionError on a tree violation
375
542
  };
543
+ // 对抗复查 A-7(HIGH):per-chunk Buffer.toString("utf8") mangles a MULTI-BYTE character that
544
+ // straddles a network chunk boundary (each half decodes to U+FFFD; the JSON often stays valid →
545
+ // the payload is SILENTLY rewritten — CJK content is the common casualty). StringDecoder holds the
546
+ // partial sequence across chunks — the standard Node fix.
376
547
  const utf8 = new StringDecoder("utf8");
377
548
  try {
378
549
  for await (const chunk of req) {
550
+ // 租约续期(见 importLeases 旁注):上传本身就是活性证据 —— 大会话的长上传不该被判成"持有者死了"。
379
551
  touchImportLease(pending.realSessionId);
380
552
  lineBuf += utf8.write(chunk);
381
553
  if (Buffer.byteLength(lineBuf, "utf8") > SYNC_NDJSON_LINE_MAX_BYTES)
@@ -385,7 +557,7 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
385
557
  const line = lineBuf.slice(0, nl).trim();
386
558
  lineBuf = lineBuf.slice(nl + 1);
387
559
  if (line.length === 0)
388
- continue;
560
+ continue; // tolerate blank lines / a trailing newline
389
561
  let e;
390
562
  try {
391
563
  e = JSON.parse(line);
@@ -399,7 +571,8 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
399
571
  await flush();
400
572
  }
401
573
  }
402
- lineBuf += utf8.end();
574
+ // any trailing partial line (no final newline) is still a complete entry to honor.
575
+ lineBuf += utf8.end(); // flush a dangling partial sequence (A-7)
403
576
  const tail = lineBuf.trim();
404
577
  if (tail.length > 0) {
405
578
  let e;
@@ -413,12 +586,21 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
413
586
  pendingBatch.push(e);
414
587
  }
415
588
  await flush();
589
+ // End-of-stream invariants (exactly-one-root + leaf-resolvable) — finish() throws SessionError on violation.
416
590
  validator.finish();
591
+ // 对抗复查 A-3(HIGH):the streamed tree must BE the Phase-A-declared one — same count, same id set.
592
+ // Without this, a caller could open a staging with ids=A (snapshots/anchors parked against A) and
593
+ // stream a different-but-valid tree B to commit; the classification and parked artifacts would
594
+ // silently reference the wrong log. Set equality (order is core-validated separately).
417
595
  if (streamedIds.size !== pending.declaredEntryIds.length || !pending.declaredEntryIds.every((id) => streamedIds.has(id))) {
418
596
  throw new PrecheckError(422, "streamed entries do not match the Phase-A declared entryIds");
419
597
  }
420
598
  }
421
599
  catch (e) {
600
+ // A transport-level PrecheckError carries its own status; a core SessionError("invalid_session") (a step()
601
+ // or finish() invariant violation) maps to 422. Either way: drop the staged rows + the lease/map entry up
602
+ // front (a malformed stream won't be retried under THIS stagingId; the client re-opens Phase A). The reaper
603
+ // is the backstop if the abort itself fails. Anything else re-throws to the outer handler.
422
604
  if (e instanceof PrecheckError) {
423
605
  await pending.handle.abort().catch(() => undefined);
424
606
  cleanupStaging(stagingId);
@@ -431,10 +613,20 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
431
613
  sendError(res, 422, "invalid_entries", "staged session log failed validation");
432
614
  return;
433
615
  }
616
+ // 对抗复查 A-4(HIGH):a NON-validation exception (storage blip, dropped connection) used to re-throw
617
+ // with the lease + pendingStagings entry INTACT — the outer handler 500s, and every later Phase A for
618
+ // this session 409s `import_in_flight` until the process restarts (the DB reaper can't clear the
619
+ // in-memory Set). Clean up here too: abort the staged rows (best-effort; reaper backstops) + release
620
+ // the lease, so the client's retry re-opens Phase A instead of wedging. Then re-throw for the 500.
434
621
  await pending.handle.abort().catch(() => undefined);
435
622
  cleanupStaging(stagingId);
436
623
  throw e;
437
624
  }
625
+ // 对抗复查 A-6(HIGH,route 半件):re-check the ACTIVE-RUN guard right before commit — Phase A's check
626
+ // can be minutes stale by the time the stream finishes, and an overwrite-dst commit would purge a log a
627
+ // LIVE run is appending to. (The owner half lives in the store commit's in-txn re-classify + FOR UPDATE;
628
+ // a residual sub-second TOCTOU between this check and the txn remains — documented, not load-bearing for
629
+ // correctness since the commit txn serializes against appends via FOR UPDATE.)
438
630
  const activeAtCommit = await deps.runStore?.getActiveTaskId(sessionId);
439
631
  if (activeAtCommit) {
440
632
  await pending.handle.abort().catch(() => undefined);
@@ -442,8 +634,13 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
442
634
  sendError(res, 409, "session_active", "session started an active run while the import streamed; sync after it settles", { activeTaskId: activeAtCommit });
443
635
  return;
444
636
  }
637
+ // ── commit (the atomic swap) → replay snapshots/policy/anchors → cache evict → release lease ──
445
638
  try {
446
639
  const committed = await pending.handle.commit(pending.owner, pending.resolution ? { resolution: pending.resolution } : undefined);
640
+ // ① snapshots — content-addressed import (idempotent). importManifest never throws → a {ok:false} is a
641
+ // fail-closed 422 (a blob the Phase-A presence check couldn't catch — corrupted in transit). Note: the
642
+ // ENTRIES are already committed (the swap), so a snapshot failure here means the conversation synced but a
643
+ // file tree did not — we surface 422 so the client re-PUTs the blob and re-POSTs (idempotent).
447
644
  if (canImportSnapshots) {
448
645
  const importManifest = deps.fileSnapshotStore.importManifest.bind(deps.fileSnapshotStore);
449
646
  for (const snap of pending.snapshots) {
@@ -455,25 +652,32 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
455
652
  }
456
653
  }
457
654
  }
655
+ // ② policy — replay each record; the E6 tighten-only gate applies unless the importer is a verified operator.
458
656
  const policyStore = deps.sessionPolicyStore;
459
657
  if (policyStore)
460
658
  for (const rec of pending.policy)
461
659
  await policyStore.putRules(sessionId, rec.principal, rec.rules, { operator: pending.operatorOk });
660
+ // ③ anchors — owner RE-KEYED to the importing principal (§9).
462
661
  const anchorStore = deps.resumeAnchorStore;
463
662
  if (anchorStore)
464
663
  for (const a of pending.anchors)
465
664
  await anchorStore.put(sessionId, a.eventId, a.entryId, pending.owner);
665
+ // The commit wrote durable state via the RAW store, bypassing the CachingSessionStore wrapper → evict the warm handle.
466
666
  deps.sessionStorage.evict?.(sessionId);
467
667
  cleanupStaging(stagingId);
468
668
  sendJson(res, 200, { relation: committed.relation });
469
669
  }
470
670
  catch (e) {
671
+ // A commit-time conflict (the in-txn re-classify flipped to fork/stale, TOCTOU close) rolled the swap back →
672
+ // the staging rows still exist → abort them; the client must restart from Phase A with a fresh resolution.
471
673
  if (e instanceof SyncConflictError) {
472
674
  await pending.handle.abort().catch(() => undefined);
473
675
  cleanupStaging(stagingId);
474
676
  sendError(res, 409, "conflict", "sync conflict", { relation: e.relation });
475
677
  return;
476
678
  }
679
+ // A SessionPolicyError happens AFTER the swap committed (the policy replay is post-commit) → the conversation
680
+ // synced, only the loosen is refused (fail-closed). The staging rows were already renamed away (abort no-op).
477
681
  if (e instanceof SessionPolicyError) {
478
682
  cleanupStaging(stagingId);
479
683
  sendError(res, e.code === "loosen_forbidden" ? 403 : 409, e.code, e.message);
@@ -484,6 +688,11 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
484
688
  }
485
689
  return;
486
690
  }
691
+ // POST …/sync/plan — DRY-RUN: classify what a PUSH of the LOCAL peer's log (body.entryIds, the OTHER process's
692
+ // entry-id list) WOULD do against the cloud's current log, WITHOUT writing. The same-process planSync(both
693
+ // backends) doesn't map the HTTP topology (one peer is remote) → the route classifies by ids: the client ids
694
+ // are the SOURCE (oldest-first), the cloud's current entries are the DST. own-or-FRESH gate (a fresh cloud
695
+ // session ⇒ the relation is `fresh`).
487
696
  if (sm[2] === "plan" && req.method === "POST") {
488
697
  if (!pushGateOk())
489
698
  return;
@@ -499,11 +708,12 @@ async function handleSessionSyncBody(req, res, url, ctx, miss) {
499
708
  sendError(res, 400, "request.field_invalid", "entryIds must be an array of strings");
500
709
  return;
501
710
  }
502
- const dst = await deps.sessionStorage?.exportEntries?.(sessionId);
711
+ const dst = await deps.sessionStorage?.exportEntries?.(sessionId); // the cloud's current log (null if fresh)
503
712
  const relation = classifySyncRelationshipByIds(body.entryIds, dst ? dst.map((e) => e.id) : null);
504
713
  sendJson(res, 200, relation);
505
714
  return;
506
715
  }
716
+ // A matched sync path but a wrong METHOD (e.g. POST /manifest) → 404 (no method-specific 405 elsewhere).
507
717
  sendError(res, 404, "not_found.route", "not found");
508
718
  return;
509
719
  }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * design/158 A9 域模块 —— 会话列表 / 发件链接列表。
3
+ *
4
+ * 路由:`GET /v1/sessions`(CC /resume picker,keyset 分页)、`GET /v1/sendfile-links`。
5
+ * owner-scope 与 trace 列表同一权威模型(system/traceToken=fleet-wide,principal=钉住本人)。本域零可变状态。
6
+ */
1
7
  import type { IncomingMessage, ServerResponse } from "node:http";
2
8
  import type { RouteCtx } from "../route-ctx.js";
3
9
  export declare function handleSessionsList(req: IncomingMessage, res: ServerResponse, url: string, ctx: RouteCtx): Promise<boolean>;