@sema-agent/server 1.323.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (539) hide show
  1. package/dist/approval-hmac.d.ts +40 -0
  2. package/dist/approval-hmac.js +67 -0
  3. package/dist/approval.d.ts +31 -0
  4. package/dist/approval.js +72 -4
  5. package/dist/audit.d.ts +26 -0
  6. package/dist/audit.js +40 -1
  7. package/dist/auth-bridge.d.ts +10 -0
  8. package/dist/auth-bridge.js +47 -4
  9. package/dist/auth-keys.d.ts +19 -0
  10. package/dist/auth-keys.js +13 -0
  11. package/dist/bake-runner/main.d.ts +5 -0
  12. package/dist/bake-runner/main.js +43 -4
  13. package/dist/bake-runner/protocol.d.ts +57 -0
  14. package/dist/bake-runner/protocol.js +58 -3
  15. package/dist/bake-runner/runner.d.ts +54 -0
  16. package/dist/bake-runner/runner.js +87 -5
  17. package/dist/bench/l8/artifact.d.ts +63 -0
  18. package/dist/bench/l8/artifact.js +61 -2
  19. package/dist/bench/l8/escape.d.ts +36 -0
  20. package/dist/bench/l8/escape.js +25 -0
  21. package/dist/bench/l8/index.d.ts +14 -0
  22. package/dist/bench/l8/index.js +14 -0
  23. package/dist/bench/l8/probes.d.ts +74 -0
  24. package/dist/bench/l8/probes.js +60 -0
  25. package/dist/bench/l8/run-probes.d.ts +43 -0
  26. package/dist/bench/l8/run-probes.js +45 -2
  27. package/dist/bench/s1/arms.d.ts +126 -0
  28. package/dist/bench/s1/arms.js +188 -13
  29. package/dist/bench/s1/live-deps.d.ts +66 -0
  30. package/dist/bench/s1/live-deps.js +249 -11
  31. package/dist/bench/s1/oracle.d.ts +49 -0
  32. package/dist/bench/s1/oracle.js +36 -1
  33. package/dist/bench/s1/repair-oracle-adapter.d.ts +33 -0
  34. package/dist/bench/s1/repair-oracle-adapter.js +30 -0
  35. package/dist/bench/s1/reviewer.d.ts +38 -0
  36. package/dist/bench/s1/reviewer.js +34 -0
  37. package/dist/bench/s1/row.d.ts +77 -0
  38. package/dist/bench/s1/row.js +62 -0
  39. package/dist/bench/s1/run-firm.d.ts +47 -0
  40. package/dist/bench/s1/run-firm.js +70 -4
  41. package/dist/bench/s1/runner-ctx.d.ts +71 -0
  42. package/dist/bench/s1/runner-ctx.js +36 -0
  43. package/dist/bench/s1/tasks.d.ts +108 -0
  44. package/dist/bench/s1/tasks.js +115 -0
  45. package/dist/boot-reclaim.d.ts +19 -0
  46. package/dist/boot-reclaim.js +20 -0
  47. package/dist/brain.d.ts +25 -0
  48. package/dist/brain.js +61 -1
  49. package/dist/budget.d.ts +66 -0
  50. package/dist/budget.js +128 -4
  51. package/dist/capabilities/builtin-tools.d.ts +3 -0
  52. package/dist/capabilities/builtin-tools.js +3 -0
  53. package/dist/capabilities/center-plugins.d.ts +17 -1
  54. package/dist/capabilities/center-plugins.js +45 -4
  55. package/dist/capabilities/center-prompts.d.ts +46 -0
  56. package/dist/capabilities/center-prompts.js +57 -3
  57. package/dist/capabilities/code-review-council.d.ts +28 -0
  58. package/dist/capabilities/code-review-council.js +40 -7
  59. package/dist/capabilities/collab-wire.d.ts +2 -0
  60. package/dist/capabilities/collab-workflows.d.ts +7 -0
  61. package/dist/capabilities/collab-workflows.js +44 -1
  62. package/dist/capabilities/oa-tools.d.ts +6 -0
  63. package/dist/capabilities/oa-tools.js +10 -0
  64. package/dist/capabilities/prompt.d.ts +14 -0
  65. package/dist/capabilities/prompt.js +14 -0
  66. package/dist/capabilities/prompts/code-review.d.ts +14 -0
  67. package/dist/capabilities/prompts/code-review.js +14 -0
  68. package/dist/capabilities/prompts/identity.d.ts +6 -0
  69. package/dist/capabilities/prompts/identity.js +6 -0
  70. package/dist/capabilities/prompts/team.d.ts +4 -0
  71. package/dist/capabilities/prompts/team.js +4 -0
  72. package/dist/capabilities/repo-tools.d.ts +11 -0
  73. package/dist/capabilities/repo-tools.js +8 -0
  74. package/dist/capabilities/sandbox-file-send.d.ts +95 -0
  75. package/dist/capabilities/sandbox-file-send.js +91 -3
  76. package/dist/capabilities/scenarios.d.ts +130 -0
  77. package/dist/capabilities/scenarios.js +180 -7
  78. package/dist/capabilities/select-environment-tool.d.ts +7 -0
  79. package/dist/capabilities/select-environment-tool.js +30 -0
  80. package/dist/capabilities/send-user-file-tool.d.ts +33 -0
  81. package/dist/capabilities/send-user-file-tool.js +65 -1
  82. package/dist/capabilities/skills.d.ts +29 -0
  83. package/dist/capabilities/skills.js +13 -2
  84. package/dist/capabilities/team.d.ts +22 -0
  85. package/dist/capabilities/team.js +22 -2
  86. package/dist/capabilities/tool-defer.d.ts +26 -0
  87. package/dist/capabilities/tool-defer.js +3 -0
  88. package/dist/config-center/apply-effective.d.ts +65 -0
  89. package/dist/config-center/apply-effective.js +204 -12
  90. package/dist/config-center/http-client.d.ts +34 -0
  91. package/dist/config-center/http-client.js +68 -0
  92. package/dist/config-center/restart-signal.d.ts +22 -0
  93. package/dist/config-center/restart-signal.js +39 -0
  94. package/dist/config-center/skills-mcp.d.ts +20 -1
  95. package/dist/config-center/skills-mcp.js +42 -7
  96. package/dist/config-center/types.d.ts +92 -1
  97. package/dist/config-lkg.d.ts +12 -0
  98. package/dist/config-lkg.js +41 -0
  99. package/dist/config-provider.d.ts +78 -0
  100. package/dist/config-provider.js +165 -2
  101. package/dist/config-types.d.ts +546 -0
  102. package/dist/config.d.ts +64 -0
  103. package/dist/config.js +455 -60
  104. package/dist/degenerate-instrument.d.ts +38 -0
  105. package/dist/degenerate-instrument.js +55 -3
  106. package/dist/elicitation.d.ts +39 -0
  107. package/dist/elicitation.js +87 -3
  108. package/dist/env-facts.d.ts +61 -0
  109. package/dist/env-facts.js +73 -7
  110. package/dist/fleet/fleet-bus.d.ts +185 -0
  111. package/dist/fleet/fleet-bus.js +333 -27
  112. package/dist/fleet/subagent-tail-bus.d.ts +11 -0
  113. package/dist/fleet/subagent-tail-bus.js +33 -2
  114. package/dist/fleet-client.d.ts +38 -0
  115. package/dist/fleet-client.js +44 -9
  116. package/dist/fleet-lease.d.ts +46 -0
  117. package/dist/fleet-lease.js +73 -5
  118. package/dist/hooks/branch-transcript.d.ts +8 -0
  119. package/dist/hooks/branch-transcript.js +33 -0
  120. package/dist/hooks/cc-stop-prompt.d.ts +42 -0
  121. package/dist/hooks/cc-stop-prompt.js +43 -1
  122. package/dist/hooks/hook-llm.d.ts +15 -0
  123. package/dist/hooks/hook-llm.js +47 -0
  124. package/dist/hooks/hook-runner.d.ts +89 -0
  125. package/dist/hooks/hook-runner.js +327 -18
  126. package/dist/http/idempotency.d.ts +31 -0
  127. package/dist/http/idempotency.js +34 -0
  128. package/dist/http/principal-gate.d.ts +17 -0
  129. package/dist/http/principal-gate.js +35 -3
  130. package/dist/http/route-ctx.d.ts +36 -0
  131. package/dist/http/routes/approvals-assistant.d.ts +25 -0
  132. package/dist/http/routes/approvals-assistant.js +253 -23
  133. package/dist/http/routes/attachments.js +22 -4
  134. package/dist/http/routes/capabilities.d.ts +7 -0
  135. package/dist/http/routes/capabilities.js +202 -1
  136. package/dist/http/routes/fleet.d.ts +6 -0
  137. package/dist/http/routes/fleet.js +94 -5
  138. package/dist/http/routes/images.d.ts +17 -0
  139. package/dist/http/routes/images.js +222 -14
  140. package/dist/http/routes/leader.d.ts +7 -0
  141. package/dist/http/routes/leader.js +10 -0
  142. package/dist/http/routes/memory-policy.d.ts +6 -0
  143. package/dist/http/routes/memory-policy.js +38 -6
  144. package/dist/http/routes/notify-wake.d.ts +10 -0
  145. package/dist/http/routes/notify-wake.js +19 -3
  146. package/dist/http/routes/observability.d.ts +6 -0
  147. package/dist/http/routes/observability.js +19 -2
  148. package/dist/http/routes/runs.d.ts +15 -0
  149. package/dist/http/routes/runs.js +464 -41
  150. package/dist/http/routes/session-sync.d.ts +15 -0
  151. package/dist/http/routes/session-sync.js +229 -19
  152. package/dist/http/routes/sessions-list.d.ts +6 -0
  153. package/dist/http/routes/sessions-list.js +35 -4
  154. package/dist/http/routes/sessions.d.ts +14 -0
  155. package/dist/http/routes/sessions.js +176 -31
  156. package/dist/http/routes/side-query.d.ts +8 -0
  157. package/dist/http/routes/side-query.js +26 -1
  158. package/dist/http/routes/tasks.d.ts +18 -0
  159. package/dist/http/routes/tasks.js +501 -38
  160. package/dist/http/routes/trace-usage.d.ts +10 -0
  161. package/dist/http/routes/trace-usage.js +66 -14
  162. package/dist/http/routes/workflows.d.ts +12 -0
  163. package/dist/http/routes/workflows.js +180 -19
  164. package/dist/http/run-meta.d.ts +11 -0
  165. package/dist/http/run-meta.js +6 -0
  166. package/dist/http/send.d.ts +19 -0
  167. package/dist/http/send.js +11 -0
  168. package/dist/http/server.d.ts +255 -0
  169. package/dist/http/server.js +921 -75
  170. package/dist/http/sse-log.d.ts +15 -0
  171. package/dist/http/sse-log.js +16 -0
  172. package/dist/http/tar.d.ts +9 -0
  173. package/dist/http/tar.js +21 -5
  174. package/dist/http/wire-gate.d.ts +9 -0
  175. package/dist/http/wire-gate.js +9 -0
  176. package/dist/http/wire-types.d.ts +187 -0
  177. package/dist/http/workspace-content.d.ts +8 -0
  178. package/dist/http/workspace-content.js +10 -0
  179. package/dist/images/bake-validate.d.ts +63 -0
  180. package/dist/images/bake-validate.js +71 -1
  181. package/dist/images/manifest.d.ts +19 -0
  182. package/dist/images/manifest.js +6 -1
  183. package/dist/index.d.ts +1 -5
  184. package/dist/index.js +10 -5
  185. package/dist/key-resolver.d.ts +23 -0
  186. package/dist/key-resolver.js +27 -2
  187. package/dist/leader/diffout.d.ts +23 -0
  188. package/dist/leader/diffout.js +8 -1
  189. package/dist/leader/diffup.d.ts +32 -0
  190. package/dist/leader/diffup.js +43 -0
  191. package/dist/leader/endpoint.d.ts +20 -0
  192. package/dist/leader/endpoint.js +25 -2
  193. package/dist/leader/fanout.d.ts +60 -0
  194. package/dist/leader/fanout.js +29 -3
  195. package/dist/leader/grader-env-factory.d.ts +74 -0
  196. package/dist/leader/grader-env-factory.js +62 -3
  197. package/dist/leader/leader.d.ts +97 -0
  198. package/dist/leader/leader.js +151 -4
  199. package/dist/leader/merge.d.ts +94 -0
  200. package/dist/leader/merge.js +112 -9
  201. package/dist/leader/planner.d.ts +52 -0
  202. package/dist/leader/planner.js +47 -3
  203. package/dist/leader/repair-oracle.d.ts +61 -0
  204. package/dist/leader/repair-oracle.js +59 -1
  205. package/dist/leader/repair-wire.d.ts +78 -0
  206. package/dist/leader/repair-wire.js +79 -2
  207. package/dist/leader/wire.d.ts +79 -0
  208. package/dist/leader/wire.js +255 -8
  209. package/dist/lsp/e2b-bridge.d.ts +16 -0
  210. package/dist/lsp/e2b-bridge.js +32 -4
  211. package/dist/lsp/e2b-manager.d.ts +10 -0
  212. package/dist/lsp/e2b-manager.js +18 -2
  213. package/dist/lsp/lsp-frames.d.ts +12 -0
  214. package/dist/lsp/lsp-frames.js +12 -0
  215. package/dist/lsp/manager.d.ts +12 -0
  216. package/dist/lsp/manager.js +35 -2
  217. package/dist/lsp/types.d.ts +10 -0
  218. package/dist/lsp/ws-transport.d.ts +12 -0
  219. package/dist/lsp/ws-transport.js +53 -5
  220. package/dist/lsp-evict.d.ts +14 -0
  221. package/dist/lsp-evict.js +15 -1
  222. package/dist/main.js +2132 -96
  223. package/dist/memory-export.d.ts +1 -0
  224. package/dist/memory-export.js +4 -0
  225. package/dist/memory-scope.d.ts +49 -0
  226. package/dist/memory-scope.js +87 -4
  227. package/dist/memory-sync-client.d.ts +24 -0
  228. package/dist/memory-sync-client.js +41 -2
  229. package/dist/memory-sync.d.ts +23 -0
  230. package/dist/memory-sync.js +70 -1
  231. package/dist/model-select.d.ts +39 -0
  232. package/dist/model-select.js +42 -1
  233. package/dist/observability/cost-quota.d.ts +20 -0
  234. package/dist/observability/cost-quota.js +4 -0
  235. package/dist/observability/cost-taxonomy.d.ts +45 -0
  236. package/dist/observability/cost-taxonomy.js +34 -0
  237. package/dist/observability/logger.d.ts +1 -0
  238. package/dist/observability/logger.js +6 -0
  239. package/dist/observability/metrics.d.ts +13 -0
  240. package/dist/observability/metrics.js +63 -0
  241. package/dist/observability/otel-exporter.d.ts +9 -0
  242. package/dist/observability/otel-exporter.js +11 -3
  243. package/dist/observability/principal-context.d.ts +9 -0
  244. package/dist/observability/principal-context.js +9 -0
  245. package/dist/observability/prompt-manifest.d.ts +37 -0
  246. package/dist/observability/prompt-manifest.js +35 -1
  247. package/dist/observability/rate-limit.d.ts +10 -0
  248. package/dist/observability/rate-limit.js +1 -0
  249. package/dist/observability/tool-trace.d.ts +24 -0
  250. package/dist/observability/tool-trace.js +41 -1
  251. package/dist/orchestration/hardened-vm-runner.d.ts +107 -0
  252. package/dist/orchestration/hardened-vm-runner.js +119 -4
  253. package/dist/orchestration/hardened-vm-worker-runner.d.ts +14 -0
  254. package/dist/orchestration/hardened-vm-worker-runner.js +26 -1
  255. package/dist/orchestration/hardened-vm-worker.js +27 -0
  256. package/dist/orchestration/subagent-steer.d.ts +44 -0
  257. package/dist/orchestration/subagent-steer.js +45 -1
  258. package/dist/orchestration/workflow-agent-steer.d.ts +54 -0
  259. package/dist/orchestration/workflow-agent-steer.js +80 -1
  260. package/dist/orchestration/workflow-completion-inbox.d.ts +189 -2
  261. package/dist/orchestration/workflow-completion-inbox.js +283 -32
  262. package/dist/orchestration/workflow-notify-journal.d.ts +132 -1
  263. package/dist/orchestration/workflow-notify-journal.js +187 -13
  264. package/dist/parked-decide.d.ts +61 -0
  265. package/dist/parked-decide.js +58 -1
  266. package/dist/per-task-image.d.ts +54 -0
  267. package/dist/per-task-image.js +41 -0
  268. package/dist/plan-cache-probe.d.ts +35 -0
  269. package/dist/plan-cache-probe.js +18 -3
  270. package/dist/plugins/approval-exemption-store.d.ts +7 -0
  271. package/dist/plugins/approval-exemption-store.js +28 -2
  272. package/dist/plugins/approval-store-sql.d.ts +64 -0
  273. package/dist/plugins/approval-store-sql.js +32 -0
  274. package/dist/plugins/background-agent-store-sql.d.ts +55 -0
  275. package/dist/plugins/background-agent-store-sql.js +91 -4
  276. package/dist/plugins/background-shell-support.d.ts +98 -0
  277. package/dist/plugins/background-shell-support.js +117 -13
  278. package/dist/plugins/blob-backend.d.ts +104 -0
  279. package/dist/plugins/blob-backend.js +163 -6
  280. package/dist/plugins/breaker-state-sql.d.ts +65 -1
  281. package/dist/plugins/breaker-state-sql.js +46 -8
  282. package/dist/plugins/caching-session-store.d.ts +42 -0
  283. package/dist/plugins/caching-session-store.js +63 -3
  284. package/dist/plugins/checkpoint-store-sql.d.ts +179 -0
  285. package/dist/plugins/checkpoint-store-sql.js +239 -11
  286. package/dist/plugins/file-outcome-sink.d.ts +4 -0
  287. package/dist/plugins/file-outcome-sink.js +9 -0
  288. package/dist/plugins/file-resume-anchor-store.d.ts +20 -0
  289. package/dist/plugins/file-resume-anchor-store.js +43 -4
  290. package/dist/plugins/file-run-store.d.ts +90 -0
  291. package/dist/plugins/file-run-store.js +145 -13
  292. package/dist/plugins/file-snapshot-store-sql.d.ts +133 -1
  293. package/dist/plugins/file-snapshot-store-sql.js +178 -10
  294. package/dist/plugins/file-workflow-journal-store.d.ts +10 -0
  295. package/dist/plugins/file-workflow-journal-store.js +10 -0
  296. package/dist/plugins/fork-routing-session-store.d.ts +47 -0
  297. package/dist/plugins/fork-routing-session-store.js +51 -3
  298. package/dist/plugins/host-platform.d.ts +69 -0
  299. package/dist/plugins/host-platform.js +81 -2
  300. package/dist/plugins/image-bake-store-sql.d.ts +136 -1
  301. package/dist/plugins/image-bake-store-sql.js +241 -7
  302. package/dist/plugins/image-index-sql.d.ts +46 -0
  303. package/dist/plugins/image-index-sql.js +113 -4
  304. package/dist/plugins/k8s-bg-scripts.d.ts +34 -0
  305. package/dist/plugins/k8s-bg-scripts.js +88 -4
  306. package/dist/plugins/k8s-exec-protocol.d.ts +33 -0
  307. package/dist/plugins/k8s-exec-protocol.js +41 -0
  308. package/dist/plugins/local-checkpoint-store.d.ts +40 -0
  309. package/dist/plugins/local-checkpoint-store.js +86 -7
  310. package/dist/plugins/local-session-store.d.ts +157 -0
  311. package/dist/plugins/local-session-store.js +289 -34
  312. package/dist/plugins/local-task-attachment-store.js +14 -2
  313. package/dist/plugins/mailbox-store-sql.d.ts +38 -0
  314. package/dist/plugins/mailbox-store-sql.js +39 -6
  315. package/dist/plugins/memory-engine-pg.d.ts +30 -0
  316. package/dist/plugins/memory-engine-pg.js +122 -4
  317. package/dist/plugins/memory-engine-tidb.d.ts +26 -0
  318. package/dist/plugins/memory-engine-tidb.js +115 -5
  319. package/dist/plugins/memory-engine-vector-util.d.ts +5 -0
  320. package/dist/plugins/memory-engine-vector-util.js +5 -0
  321. package/dist/plugins/memory-resume-anchor-store.d.ts +8 -0
  322. package/dist/plugins/memory-resume-anchor-store.js +17 -0
  323. package/dist/plugins/memory-run-store.d.ts +63 -0
  324. package/dist/plugins/memory-run-store.js +73 -8
  325. package/dist/plugins/memory-session-policy-store.d.ts +17 -0
  326. package/dist/plugins/memory-session-policy-store.js +17 -0
  327. package/dist/plugins/memory-sync-store-pg.d.ts +42 -0
  328. package/dist/plugins/memory-sync-store-pg.js +45 -2
  329. package/dist/plugins/memory-sync-store-tidb.d.ts +9 -0
  330. package/dist/plugins/memory-sync-store-tidb.js +31 -1
  331. package/dist/plugins/outcome-ledger-sql.d.ts +63 -0
  332. package/dist/plugins/outcome-ledger-sql.js +97 -3
  333. package/dist/plugins/pg-approval-store.d.ts +7 -0
  334. package/dist/plugins/pg-approval-store.js +7 -0
  335. package/dist/plugins/pg-breaker-state.d.ts +6 -0
  336. package/dist/plugins/pg-breaker-state.js +6 -0
  337. package/dist/plugins/pg-checkpoint-store.d.ts +8 -0
  338. package/dist/plugins/pg-checkpoint-store.js +8 -0
  339. package/dist/plugins/pg-cost-quota.d.ts +17 -0
  340. package/dist/plugins/pg-cost-quota.js +6 -0
  341. package/dist/plugins/pg-file-snapshot-store.d.ts +6 -0
  342. package/dist/plugins/pg-file-snapshot-store.js +6 -0
  343. package/dist/plugins/pg-image-bake.d.ts +9 -0
  344. package/dist/plugins/pg-image-bake.js +9 -0
  345. package/dist/plugins/pg-image-index.d.ts +9 -0
  346. package/dist/plugins/pg-image-index.js +9 -0
  347. package/dist/plugins/pg-outcome-ledger.d.ts +9 -0
  348. package/dist/plugins/pg-outcome-ledger.js +9 -0
  349. package/dist/plugins/pg-pool.d.ts +50 -0
  350. package/dist/plugins/pg-pool.js +78 -0
  351. package/dist/plugins/pg-query.d.ts +11 -0
  352. package/dist/plugins/pg-rate-limiter.d.ts +23 -0
  353. package/dist/plugins/pg-rate-limiter.js +12 -2
  354. package/dist/plugins/pg-resume-anchor-store.d.ts +5 -0
  355. package/dist/plugins/pg-resume-anchor-store.js +5 -0
  356. package/dist/plugins/pg-run-store.d.ts +7 -0
  357. package/dist/plugins/pg-run-store.js +7 -0
  358. package/dist/plugins/pg-safe-json.d.ts +13 -0
  359. package/dist/plugins/pg-safe-json.js +40 -4
  360. package/dist/plugins/pg-session-policy-store.d.ts +5 -0
  361. package/dist/plugins/pg-session-policy-store.js +5 -0
  362. package/dist/plugins/pg-session-storage.d.ts +72 -0
  363. package/dist/plugins/pg-session-storage.js +152 -20
  364. package/dist/plugins/pg-session-store.d.ts +10 -0
  365. package/dist/plugins/pg-session-store.js +10 -0
  366. package/dist/plugins/pg-tool-result-store.d.ts +7 -0
  367. package/dist/plugins/pg-tool-result-store.js +7 -0
  368. package/dist/plugins/pg-workflow-journal-store.d.ts +7 -0
  369. package/dist/plugins/pg-workflow-journal-store.js +7 -0
  370. package/dist/plugins/pg-workflow-run-store.d.ts +7 -0
  371. package/dist/plugins/pg-workflow-run-store.js +7 -0
  372. package/dist/plugins/posix-shell-fs.d.ts +3 -0
  373. package/dist/plugins/posix-shell-fs.js +31 -1
  374. package/dist/plugins/remote-env-adb.d.ts +25 -0
  375. package/dist/plugins/remote-env-adb.js +95 -12
  376. package/dist/plugins/remote-env-e2b.d.ts +146 -0
  377. package/dist/plugins/remote-env-e2b.js +371 -40
  378. package/dist/plugins/remote-env-file-error.d.ts +34 -0
  379. package/dist/plugins/remote-env-file-error.js +37 -0
  380. package/dist/plugins/remote-env-host.d.ts +151 -0
  381. package/dist/plugins/remote-env-host.js +468 -70
  382. package/dist/plugins/remote-env-k8s.d.ts +135 -0
  383. package/dist/plugins/remote-env-k8s.js +318 -32
  384. package/dist/plugins/remote-env-local-docker.d.ts +65 -0
  385. package/dist/plugins/remote-env-local-docker.js +149 -24
  386. package/dist/plugins/remote-env-ssh.d.ts +35 -0
  387. package/dist/plugins/remote-env-ssh.js +117 -20
  388. package/dist/plugins/remote-scratchpad.d.ts +38 -0
  389. package/dist/plugins/remote-scratchpad.js +32 -2
  390. package/dist/plugins/remote-shell.d.ts +22 -0
  391. package/dist/plugins/remote-shell.js +27 -1
  392. package/dist/plugins/resume-anchor-store-sql.d.ts +28 -0
  393. package/dist/plugins/resume-anchor-store-sql.js +11 -0
  394. package/dist/plugins/roster-store-sql.d.ts +27 -0
  395. package/dist/plugins/roster-store-sql.js +54 -0
  396. package/dist/plugins/run-store-sql.d.ts +194 -0
  397. package/dist/plugins/run-store-sql.js +210 -9
  398. package/dist/plugins/s3-presign.d.ts +20 -0
  399. package/dist/plugins/s3-presign.js +49 -2
  400. package/dist/plugins/scheduler-support.d.ts +12 -0
  401. package/dist/plugins/scheduler-support.js +75 -3
  402. package/dist/plugins/send-file-ledger.d.ts +15 -0
  403. package/dist/plugins/send-file-ledger.js +53 -4
  404. package/dist/plugins/send-user-file.d.ts +77 -0
  405. package/dist/plugins/send-user-file.js +92 -4
  406. package/dist/plugins/session-policy-store-sql.d.ts +15 -0
  407. package/dist/plugins/session-policy-store-sql.js +75 -3
  408. package/dist/plugins/session-store.d.ts +45 -0
  409. package/dist/plugins/session-store.js +51 -0
  410. package/dist/plugins/sql-driver.d.ts +77 -0
  411. package/dist/plugins/sql-driver.js +4 -0
  412. package/dist/plugins/sql-escape.d.ts +8 -0
  413. package/dist/plugins/sql-escape.js +8 -0
  414. package/dist/plugins/sql-row-helpers.d.ts +25 -0
  415. package/dist/plugins/sql-row-helpers.js +25 -0
  416. package/dist/plugins/store-backend.d.ts +103 -0
  417. package/dist/plugins/store-backend.js +120 -27
  418. package/dist/plugins/store-contracts.d.ts +62 -0
  419. package/dist/plugins/store-contracts.js +3 -0
  420. package/dist/plugins/task-attachment-store.d.ts +60 -0
  421. package/dist/plugins/task-attachment-store.js +49 -3
  422. package/dist/plugins/task-list-store-sql.d.ts +25 -0
  423. package/dist/plugins/task-list-store-sql.js +21 -0
  424. package/dist/plugins/tidb-approval-store.d.ts +6 -0
  425. package/dist/plugins/tidb-approval-store.js +6 -0
  426. package/dist/plugins/tidb-breaker-state.d.ts +5 -0
  427. package/dist/plugins/tidb-breaker-state.js +5 -0
  428. package/dist/plugins/tidb-checkpoint-store.d.ts +7 -0
  429. package/dist/plugins/tidb-checkpoint-store.js +7 -0
  430. package/dist/plugins/tidb-cost-quota.d.ts +6 -0
  431. package/dist/plugins/tidb-cost-quota.js +1 -0
  432. package/dist/plugins/tidb-file-snapshot-store.d.ts +6 -0
  433. package/dist/plugins/tidb-file-snapshot-store.js +6 -0
  434. package/dist/plugins/tidb-image-bake.d.ts +8 -0
  435. package/dist/plugins/tidb-image-bake.js +8 -0
  436. package/dist/plugins/tidb-image-index.d.ts +8 -0
  437. package/dist/plugins/tidb-image-index.js +8 -0
  438. package/dist/plugins/tidb-outcome-ledger.d.ts +10 -0
  439. package/dist/plugins/tidb-outcome-ledger.js +10 -0
  440. package/dist/plugins/tidb-pool.d.ts +56 -0
  441. package/dist/plugins/tidb-pool.js +181 -1
  442. package/dist/plugins/tidb-rate-limiter.d.ts +11 -0
  443. package/dist/plugins/tidb-rate-limiter.js +5 -2
  444. package/dist/plugins/tidb-resume-anchor-store.d.ts +5 -0
  445. package/dist/plugins/tidb-resume-anchor-store.js +5 -0
  446. package/dist/plugins/tidb-run-store.d.ts +7 -0
  447. package/dist/plugins/tidb-run-store.js +7 -0
  448. package/dist/plugins/tidb-session-policy-store.d.ts +5 -0
  449. package/dist/plugins/tidb-session-policy-store.js +5 -0
  450. package/dist/plugins/tidb-session-storage.d.ts +43 -0
  451. package/dist/plugins/tidb-session-storage.js +62 -3
  452. package/dist/plugins/tidb-session-store.d.ts +142 -0
  453. package/dist/plugins/tidb-session-store.js +264 -16
  454. package/dist/plugins/tidb-tool-result-store.d.ts +5 -0
  455. package/dist/plugins/tidb-tool-result-store.js +7 -0
  456. package/dist/plugins/tidb-workflow-journal-store.d.ts +7 -0
  457. package/dist/plugins/tidb-workflow-journal-store.js +7 -0
  458. package/dist/plugins/tidb-workflow-run-store.d.ts +8 -0
  459. package/dist/plugins/tidb-workflow-run-store.js +8 -0
  460. package/dist/plugins/tool-result-store-sql.d.ts +58 -0
  461. package/dist/plugins/tool-result-store-sql.js +38 -2
  462. package/dist/plugins/web-search.d.ts +35 -0
  463. package/dist/plugins/web-search.js +45 -9
  464. package/dist/plugins/workflow-journal-limits.d.ts +10 -0
  465. package/dist/plugins/workflow-journal-limits.js +10 -0
  466. package/dist/plugins/workflow-journal-store-sql.d.ts +78 -0
  467. package/dist/plugins/workflow-journal-store-sql.js +58 -2
  468. package/dist/plugins/workflow-run-store-sql.d.ts +84 -0
  469. package/dist/plugins/workflow-run-store-sql.js +84 -6
  470. package/dist/plugins/worktree-isolation.d.ts +45 -0
  471. package/dist/plugins/worktree-isolation.js +105 -6
  472. package/dist/plugins/write-behind-counter.d.ts +56 -1
  473. package/dist/plugins/write-behind-counter.js +53 -14
  474. package/dist/principal-jwt.d.ts +24 -0
  475. package/dist/principal-jwt.js +60 -5
  476. package/dist/project-memory.d.ts +22 -0
  477. package/dist/project-memory.js +144 -15
  478. package/dist/prompts-domain-validate.d.ts +28 -0
  479. package/dist/prompts-domain-validate.js +42 -1
  480. package/dist/question.d.ts +33 -0
  481. package/dist/question.js +69 -2
  482. package/dist/resource-suspend.d.ts +21 -0
  483. package/dist/resource-suspend.js +16 -0
  484. package/dist/router/route-orchestration.d.ts +78 -0
  485. package/dist/router/route-orchestration.js +77 -0
  486. package/dist/run-local.d.ts +32 -0
  487. package/dist/run-local.js +172 -8
  488. package/dist/runs.d.ts +189 -4
  489. package/dist/runs.js +465 -24
  490. package/dist/runtime-caps-resolver.d.ts +106 -0
  491. package/dist/runtime-caps-resolver.js +89 -4
  492. package/dist/runtime-governance.d.ts +84 -0
  493. package/dist/runtime-governance.js +127 -1
  494. package/dist/sandbox-pkg-source.d.ts +23 -0
  495. package/dist/sandbox-pkg-source.js +37 -0
  496. package/dist/sealed-key.d.ts +54 -0
  497. package/dist/sealed-key.js +68 -3
  498. package/dist/security.d.ts +108 -0
  499. package/dist/security.js +145 -6
  500. package/dist/sema-registry.d.ts +21 -0
  501. package/dist/sema-registry.js +21 -0
  502. package/dist/session-leaf-bus.d.ts +31 -0
  503. package/dist/session-leaf-bus.js +32 -0
  504. package/dist/session-sync-content.d.ts +46 -0
  505. package/dist/session-sync-content.js +47 -1
  506. package/dist/session-sync-kernel.d.ts +89 -0
  507. package/dist/session-sync-kernel.js +48 -3
  508. package/dist/session-sync.d.ts +125 -0
  509. package/dist/session-sync.js +181 -8
  510. package/dist/session-titler.d.ts +26 -0
  511. package/dist/session-titler.js +41 -7
  512. package/dist/session-watch.d.ts +53 -1
  513. package/dist/session-watch.js +73 -9
  514. package/dist/sighup-idle.d.ts +30 -0
  515. package/dist/sighup-idle.js +12 -1
  516. package/dist/spec-fields.d.ts +91 -3
  517. package/dist/spec-fields.js +116 -4
  518. package/dist/task-cwd.d.ts +50 -0
  519. package/dist/task-cwd.js +68 -1
  520. package/dist/task-mcp.d.ts +47 -0
  521. package/dist/task-mcp.js +29 -0
  522. package/dist/task-settings.d.ts +175 -0
  523. package/dist/task-settings.js +226 -11
  524. package/dist/task-workflow.d.ts +67 -0
  525. package/dist/task-workflow.js +75 -8
  526. package/dist/tool-approval.d.ts +91 -0
  527. package/dist/tool-approval.js +215 -0
  528. package/dist/trace/artifacts.d.ts +9 -0
  529. package/dist/trace/artifacts.js +30 -5
  530. package/dist/trace/core-keyset-guard.d.ts +18 -0
  531. package/dist/trace/ledger-sink.d.ts +43 -0
  532. package/dist/trace/ledger-sink.js +64 -5
  533. package/dist/trace/project.d.ts +133 -0
  534. package/dist/trace/project.js +189 -2
  535. package/dist/trace/redact.d.ts +19 -0
  536. package/dist/trace/redact.js +66 -9
  537. package/dist/usage-analytics.d.ts +26 -0
  538. package/dist/usage-analytics.js +26 -3
  539. package/package.json +1 -1
@@ -1,10 +1,42 @@
1
+ /**
2
+ * LOCAL session store — the DB-less twin of {@link TiDBSessionStore} (clay 2026-06-25 seamless local↔cloud). Wraps a
3
+ * core `SessionRepo` (in-memory `InMemorySessionRepo` for v1; `FileSessionRepo` for the file-backed follow-on — both
4
+ * expose create/open/list/delete/fork) and projects it into the service's `SessionStore` + `OwnerAwareSessionStore`
5
+ * shape, so a locally-run HTTP service serves the SAME session contract a cloud worker does: acquire (the run path) +
6
+ * the §0.5 ownership ops (ownerOf/register/fork/deleteSession).
7
+ *
8
+ * Single-user by construction: a local file/in-memory deployment has no multi-tenant owner column (core's
9
+ * SessionMetadata is `{id, createdAt}`), so REQUIRE_PRINCIPAL must be OFF (main.ts enforces this) and owner is tracked
10
+ * here only as the service-side scope (null/"local").
11
+ *
12
+ * P0.5 variant-2 (clay 2026-06-26): `listSessions` IS implemented — it enumerates the repo's persisted sessions
13
+ * (`repo.list()`) and synthesizes a {@link SessionSummary} per id from the SESSION abstraction (no run ledger), so
14
+ * GET /v1/sessions (server.ts: `sessionStorage.listSessions ?? runStore.listSessions`) takes the FIRST branch and the
15
+ * resume picker is restart-visible when the repo is the file-backed `FileSessionRepo`. Run-ledger-only fields
16
+ * (`runCount`/`lastStatus`) are honestly DEGRADED (the session abstraction has no runs); preview is synthesized from
17
+ * the last user-text entry. The in-memory `InMemorySessionRepo` works too (process-local; lost on restart) — the
18
+ * persistence comes from the repo, not this wrapper.
19
+ */
1
20
  import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
21
  import { join, resolve } from "node:path";
22
+ // 🔴 生产级兼容性 bug 撞获(2026-07-25,sema-desktop PoC 全新 npm install 复现):`Session` 在这个文件里
23
+ // 只当类型用(见下方 `as Session` 断言),此前却写成值导入——本地锁定 core@1.401.0 时 `Session` 恰好还是
24
+ // runtime 导出,掩盖了这个问题;但 core 1.403+(黑板 [1628]/[1634])把 `Session` 降级成纯类型、改用
25
+ // `StoredSession` 作 runtime class。任何新装 `@sema-agent/server` 在这行 import 会直接
26
+ // `SyntaxError: does not provide an export named 'Session'` 崩溃启动。改成 `import type` 消除。
27
+ // ✅ 尾声(design/158 F-5 核账,2026-07-28):上文历史版本提到的「三处 runtime `new Session(...)`
28
+ // 待迁移 + 依赖收紧 >=1.401.0 <1.403.0」**均已完成**——全仓 `new Session(` 现为零命中(三处都已迁
29
+ // `StoredSession`),依赖范围现为 `^1.444.0`。此注释保留前半段是因为「值导入 vs 类型导入被本地锁定
30
+ // 版本掩盖」这个缺陷机制仍值得后人读;宣称"待办"的部分已按实况改写(注释宣称≠代码实做是本仓
31
+ // 已立案的病族,过期待办比没有更有害)。
3
32
  import { SessionError, validateEntriesForImport } from "@sema-agent/core";
4
33
  import { identicalIdsAlsoIdenticalContent } from "../session-sync-content.js";
5
34
  import { classifySyncRelationshipByIds, SyncConflictError, stagingIdFor, } from "../session-sync-kernel.js";
6
35
  const ownerEq = (a, b) => (a ?? null) === (b ?? null);
7
36
  const isNotFound = (e) => e instanceof SessionError && e.code === "not_found";
37
+ /** Extract the plain text of a USER message entry (the objective preview source) — `null` for any other entry
38
+ * type or a non-user message, so {@link LocalSessionStore.previewOf} walks past tool/assistant turns to the last
39
+ * thing the human asked. content may be a bare string or a TextContent[] (join the text parts). */
8
40
  function userText(entry) {
9
41
  if (entry.type !== "message")
10
42
  return null;
@@ -24,34 +56,88 @@ function userText(entry) {
24
56
  }
25
57
  return null;
26
58
  }
59
+ /** 🔴 [1684] MODULE-level, keyed by the RESOLVED sidecar path: the authority is the file, so every store instance
60
+ * pointing at that file must share one view + one lock. Keyed by `resolve()`d path so `./x` and `x` collapse too. */
27
61
  const TITLE_STATE_BY_PATH = new Map();
28
62
  export class LocalSessionStore {
29
63
  repo;
64
+ /** In-flight acquisitions keyed by id, so concurrent acquire(sameId) in one process share one (mirrors TiDBSessionStore). */
30
65
  pending = new Map();
66
+ /** Service-side owner scope per session (core's repo has no owner column). Registered-ownerless ⇒ null; absent ⇒ undefined. */
31
67
  owners = new Map();
68
+ /** Last-activity marker (touch) for ordering — the keyset sort key for {@link listSessions}. PERSISTED to a JSON
69
+ * sidecar under {@link dataDir} (when given) so the /resume picker's newest-first order SURVIVES a service restart:
70
+ * without persistence the map starts EMPTY on boot and every session degrades to `createdAt` ordering (the picker
71
+ * shows the wrong "most recent" — TOC-review #10). In-memory only (no sidecar) when `dataDir` is absent (the
72
+ * process-local `InMemorySessionRepo` case, where the sessions themselves are lost on restart anyway). */
32
73
  lastActivity = new Map();
74
+ /** K-5c (core 1.155 SessionStore.noteTaskRun seam): the latest task run per session, recorded by core's Runner at
75
+ * runTask START (so a running/suspended run surfaces too) → projected as `lastRunId` on listSessions. The cloud
76
+ * TiDB/PG twins derive it from `task_run`; the local backend has no runs ledger, so this is an in-memory map.
77
+ * ⚠️ IN-MEMORY BEST-EFFORT (honest degrade, NOT sidecar-persisted — review Q6): it covers the PRIMARY /resume use
78
+ * case (re-attach to a RUNNING run — recorded at the run's start, the process can't have restarted while that run
79
+ * is still live), and is LOST on a process restart. That's acceptable: a session whose last run predates a restart
80
+ * has only TERMINAL runs (no live tail to re-attach), so `lastRunId` falls back to null and the shell uses its
81
+ * no-anchor path — same posture as the other local degrades here (runCount:0, lastStatus:""). Persisting it would
82
+ * need a sidecar-format change (the #10 clobber area); deferred until a real local-file-backed need. */
33
83
  lastTaskRun = new Map();
84
+ /** 2c P1d-β — live staged-import handles keyed by stagingId, so a local staging's in-memory buffer is readable via
85
+ * readStagedEntries (the durable twins re-read the staged session_event rows; local has no such rows). */
34
86
  stagings = new Map();
87
+ /** Lazy one-shot load of the persisted lastActivity sidecar (memoized; only the first reader pays the read). */
35
88
  loaded;
89
+ /** A pending debounced sidecar flush (coalesces a burst of touch() into one write). undefined ⇒ no flush queued. */
36
90
  flushTimer;
91
+ /** Serializes sidecar writes (bug B): the in-flight write promise. A flush() while one is running CHAINS after it
92
+ * (re-checking the dirty flag) so an older snapshot can never rename after a newer one (lost-update). */
37
93
  writeChain = Promise.resolve();
94
+ /** Set by touch/delete when the map mutated; cleared by a write that captured the change. A write that ran while this
95
+ * is set re-runs (a touch that landed during an active write isn't dropped). Drives the chain's re-run guard (bug B). */
38
96
  dirty = false;
97
+ /** Monotonic per-write counter — feeds a UNIQUE tmp filename per write so overlapping writes can't share one tmp path
98
+ * and tear each other's bytes (bug B). */
39
99
  writeSeq = 0;
100
+ /** Last time a sidecar-write error was warned (bug D): throttle the best-effort warn so a persistently-failing disk
101
+ * doesn't spam the log on every debounced flush. 0 ⇒ never warned. */
40
102
  lastWarnAt = 0;
103
+ /** Absolute path to the lastActivity JSON sidecar, or undefined when not persisting (in-memory-only deployment). */
41
104
  sidecarPath;
105
+ /** Auto-title map + its own LIGHT sidecar (`session-titles.json`). Separate file from the
106
+ * lastActivity sidecar ON PURPOSE (that format is a #10 clobber area); titles are write-once + ultra-low-rate
107
+ * (one per session, ever), so a plain read-modify-write with tmp+rename per write is enough — no debounce.
108
+ *
109
+ * 🔴 CROSS-INSTANCE (fixed 2026-07-25, blackboard [1684] — core's "per-instance state" defect class):
110
+ * these three used to be INSTANCE fields, which is exactly the shape core described — «the backend is shared
111
+ * more widely than the object, but the state and the lock hang off the object». Two `LocalSessionStore`s over
112
+ * the SAME dataDir each held their own `titles` map and their own write chain, so a real concurrency probe
113
+ * (`test/file-stores-cross-instance-concurrency.test.ts`) showed **BOTH winning** the write-once IS-NULL gate
114
+ * (`wins=2`): each checked its own map, both saw absent, both returned `true`, and the second whole-map
115
+ * snapshot silently clobbered the first title. That is core's A-class (CAS double-win) and B-class
116
+ * (lost update) stacked. Fix per core's prescription: state + lock live at MODULE level, keyed by the resolved
117
+ * sidecar path, so any number of instances over one dataDir collapse to a single authority.
118
+ * Sharing key = the resolved path; NO sidecar (in-memory deployment) ⇒ no shared backend exists ⇒ that case
119
+ * keeps genuinely per-instance state (see {@link titleState}). */
42
120
  titleSidecarPath;
121
+ /** Per-instance fallback used ONLY when there is no sidecar path (nothing is shared, so nothing to collapse). */
43
122
  ownTitleState = { titles: new Map(), writeChain: Promise.resolve() };
123
+ /** @param repo the core SessionRepo this store projects into the §0.5 SessionStore shape.
124
+ * @param dataDir the SAME data dir the file-backed repo persists under (LocalBackend passes `fileBackend.root`).
125
+ * When given, lastActivity is persisted to `<dataDir>/session-last-activity.json` so it survives a
126
+ * restart (the file-backed deployment); omit it for the in-memory deployment (nothing survives). */
44
127
  constructor(repo, dataDir) {
45
128
  this.repo = repo;
46
129
  this.sidecarPath = dataDir ? join(dataDir, "session-last-activity.json") : undefined;
130
+ // [1684] resolve() 规范化后当共享键:`./x` 与 `x` 指同一文件,必须塌缩到同一权威。
47
131
  this.titleSidecarPath = dataDir ? resolve(join(dataDir, "session-titles.json")) : undefined;
48
132
  if (this.titleSidecarPath && !TITLE_STATE_BY_PATH.has(this.titleSidecarPath)) {
49
133
  TITLE_STATE_BY_PATH.set(this.titleSidecarPath, { titles: new Map(), writeChain: Promise.resolve() });
50
134
  }
51
135
  }
136
+ /** 该实例应当使用的 title 状态:有 sidecar ⇒ 按路径共享的那份;无 sidecar ⇒ 自己那份(无共享后端)。 */
52
137
  get titleState() {
53
138
  return this.titleSidecarPath ? TITLE_STATE_BY_PATH.get(this.titleSidecarPath) : this.ownTitleState;
54
139
  }
140
+ /** Lazy one-shot hydrate of the persisted titles (mirrors ensureLoaded's posture: missing file silent, torn warn). */
55
141
  async ensureTitlesLoaded() {
56
142
  if (!this.titleSidecarPath)
57
143
  return;
@@ -63,7 +149,7 @@ export class LocalSessionStore {
63
149
  const parsed = JSON.parse(await readFile(path, "utf8"));
64
150
  for (const [id, t] of Object.entries(parsed)) {
65
151
  if (typeof t === "string" && !st.titles.has(id))
66
- st.titles.set(id, t);
152
+ st.titles.set(id, t); // live writes win over disk
67
153
  }
68
154
  }
69
155
  catch (e) {
@@ -74,6 +160,8 @@ export class LocalSessionStore {
74
160
  }
75
161
  return st.loaded;
76
162
  }
163
+ /** Chained snapshot write of the titles sidecar (the chain guarantees the LAST rename carries the
164
+ * NEWEST map state: each link re-snapshots the SHARED map at write time, so an overtaken writer is harmless). */
77
165
  persistTitles() {
78
166
  if (!this.titleSidecarPath)
79
167
  return Promise.resolve();
@@ -91,10 +179,15 @@ export class LocalSessionStore {
91
179
  });
92
180
  return st.writeChain;
93
181
  }
182
+ /** Cheap pre-LLM probe (local twin). The map write needs no session_meta row, so there is
183
+ * no "none" (register-lag) dimension here — two states only. */
94
184
  async probeTitle(sessionId) {
95
185
  await this.ensureTitlesLoaded();
96
186
  return this.titleState.titles.has(sessionId) ? "titled" : "untitled";
97
187
  }
188
+ /** Write-once auto-title (the local twin of the SQL setTitleIfNull). Persists the whole
189
+ * (small) map via a SERIALIZED tmp+rename snapshot; a write failure keeps the in-memory title (degrades to
190
+ * re-title after restart). */
98
191
  async setTitleIfNull(sessionId, title) {
99
192
  await this.ensureTitlesLoaded();
100
193
  const st = this.titleState;
@@ -104,6 +197,9 @@ export class LocalSessionStore {
104
197
  await this.persistTitles();
105
198
  return true;
106
199
  }
200
+ /** Best-effort, one-shot hydrate of the persisted lastActivity map (no-op when not persisting / no sidecar yet). A
201
+ * read/parse error degrades to an empty map (createdAt ordering) rather than failing the list — the picker still
202
+ * works, just without restart-persisted recency. Memoized so concurrent listSessions share one read. */
107
203
  async ensureLoaded() {
108
204
  if (!this.sidecarPath)
109
205
  return;
@@ -114,11 +210,14 @@ export class LocalSessionStore {
114
210
  const raw = await readFile(path, "utf8");
115
211
  const parsed = JSON.parse(raw);
116
212
  for (const [id, ts] of Object.entries(parsed)) {
213
+ // Don't clobber an in-process touch() that landed before the lazy load ran (live writes win over disk).
117
214
  if (typeof ts === "string" && !this.lastActivity.has(id))
118
215
  this.lastActivity.set(id, ts);
119
216
  }
120
217
  }
121
218
  catch (e) {
219
+ // missing sidecar (first boot) is EXPECTED — silent; a torn file / permission error degrades to an empty map
220
+ // (createdAt ordering) but is WARNED (throttled, bug D) so disk-full/permission isn't hidden. Never throws.
122
221
  if (e?.code !== "ENOENT")
123
222
  this.warnSidecar("read", e);
124
223
  }
@@ -126,42 +225,59 @@ export class LocalSessionStore {
126
225
  }
127
226
  return this.loaded;
128
227
  }
228
+ /** Queue a debounced, best-effort atomic write of the lastActivity map to the sidecar (coalesces touch bursts). The
229
+ * write is fire-and-forget: a persistence failure must NEVER break the run path (touch is on the hot acquire path),
230
+ * so errors are swallowed — the worst case is a restart falling back to createdAt ordering for un-flushed touches. */
129
231
  scheduleFlush() {
130
- this.dirty = true;
232
+ this.dirty = true; // a mutation landed — even mid-write, so the active write re-runs (bug B: don't drop it)
131
233
  if (!this.sidecarPath || this.flushTimer)
132
234
  return;
133
235
  this.flushTimer = setTimeout(() => {
134
236
  this.flushTimer = undefined;
135
237
  void this.flush();
136
238
  }, 250);
137
- this.flushTimer.unref?.();
138
- }
239
+ this.flushTimer.unref?.(); // don't keep the process alive solely for a pending flush
240
+ }
241
+ /** Atomic best-effort write of the lastActivity map. SERIALIZED via {@link writeChain} (bug B): overlapping flushes
242
+ * chain instead of racing, so an older snapshot can never rename after a newer one (lost-update), and a unique tmp
243
+ * name per write means two writes can't tear each other's bytes. Returns the chain so callers (dispose) can await the
244
+ * final write (bug C). Each link HYDRATES the disk sidecar first (bug A) and writes the UNION of disk + in-process —
245
+ * the `has(id)` live-wins guard in ensureLoaded() keeps in-process touches authoritative — so a fresh-boot flush
246
+ * before any list never CLOBBERS the prior boot's recency for not-yet-touched sessions. */
139
247
  flush() {
140
248
  if (!this.sidecarPath)
141
249
  return Promise.resolve();
142
250
  const next = this.writeChain.then(() => this.doFlush());
143
- this.writeChain = next.catch(() => { });
251
+ this.writeChain = next.catch(() => { }); // a failed write must not poison the chain (next flush still runs)
144
252
  return next;
145
253
  }
254
+ /** One serialized sidecar write: hydrate-then-merge (bug A), snapshot, atomic tmp+rename with a UNIQUE tmp name (bug
255
+ * B). Clears {@link dirty} for the change it captured; re-runs once if a touch landed mid-write so it's not dropped. */
146
256
  async doFlush() {
147
257
  const path = this.sidecarPath;
148
258
  if (!path)
149
259
  return;
260
+ // bug A: merge the on-disk sidecar BEFORE snapshotting so we never overwrite another boot's recency with an empty
261
+ // (or partial) in-process map. ensureLoaded()'s `!has(id)` guard keeps live in-process touches authoritative.
150
262
  await this.ensureLoaded();
151
- this.dirty = false;
263
+ this.dirty = false; // capture: a touch AFTER this point flips it back on → we re-run below
152
264
  const snapshot = Object.fromEntries(this.lastActivity);
153
265
  try {
154
266
  await mkdir(join(path, ".."), { recursive: true });
267
+ // bug B: unique tmp per write (pid + monotonic counter) — overlapping/successive writes can't share one tmp path.
155
268
  const tmp = `${path}.${process.pid}.${++this.writeSeq}.tmp`;
156
269
  await writeFile(tmp, JSON.stringify(snapshot), "utf8");
157
- await rename(tmp, path);
270
+ await rename(tmp, path); // atomic swap (POSIX rename) — readers see the whole file or the prior one
158
271
  }
159
272
  catch (e) {
160
- this.warnSidecar("write", e);
273
+ this.warnSidecar("write", e); // bug D: surface disk-full/permission (throttled) instead of silent swallow
274
+ // best-effort: a persistence failure degrades restart-recency, never breaks the run path.
161
275
  }
162
276
  if (this.dirty)
163
- await this.doFlush();
277
+ await this.doFlush(); // a touch landed during the write — re-run so it isn't lost (bug B)
164
278
  }
279
+ /** Throttled best-effort warn for a sidecar I/O failure (bug D): never throws, at most once per ~minute so a
280
+ * persistently-failing disk doesn't spam the log on every debounced flush. */
165
281
  warnSidecar(op, e) {
166
282
  const now = Date.now();
167
283
  if (now - this.lastWarnAt < 60_000)
@@ -176,6 +292,9 @@ export class LocalSessionStore {
176
292
  this.owners.set(id, null);
177
293
  return { session: session, sessionId: id };
178
294
  }
295
+ // design/114 Phase3 (F1): a `requireExisting` acquire NEVER creates and can REJECT (not_found) — it MUST bypass
296
+ // the shared create-dedup `pending`, else a concurrent create-capable waiter awaiting that same promise would
297
+ // inherit the not_found rejection instead of creating-on-miss. (Twin of TiDBSessionStore.acquire.)
179
298
  if (opts?.requireExisting)
180
299
  return this.load(sessionId, true);
181
300
  const inflight = this.pending.get(sessionId);
@@ -186,6 +305,8 @@ export class LocalSessionStore {
186
305
  return p;
187
306
  }
188
307
  async load(sessionId, requireExisting = false) {
308
+ // open() throws SessionError("not_found") for an unknown id — create it ownerless then (a caller-chosen brand-new
309
+ // id the service didn't pre-register; only the caller who chose it can reach it, and it holds no prior data).
189
310
  try {
190
311
  const session = await this.repo.open({ id: sessionId, createdAt: "" });
191
312
  if (!this.owners.has(sessionId))
@@ -195,17 +316,28 @@ export class LocalSessionStore {
195
316
  catch (e) {
196
317
  if (!isNotFound(e))
197
318
  throw e;
319
+ // design/114 Phase3: `requireExisting` ⇒ FAIL LOUD on a genuinely missing session, never the silent
320
+ // create-on-miss. Re-throw the store's own `not_found` SessionError; core maps it to `resume.session_not_found`.
198
321
  if (requireExisting)
199
322
  throw e;
200
323
  const session = await this.repo.create({ id: sessionId });
324
+ // GUARD: mirror the open branch — NEVER clobber an owner a prior register() recorded
325
+ // (the TiDB twin's load() re-registers ownerless via an idempotent no-op upsert). Without the guard, a
326
+ // register(id, principal)-then-acquire(id) [the §0.5 auth flow] would reset owner→null on first acquire →
327
+ // ownership gate goes silent → cross-principal attach. (register() now also creates the repo session, so this
328
+ // catch only fires for a genuinely-unregistered id — but guard it defensively regardless.)
201
329
  if (!this.owners.has(sessionId))
202
330
  this.owners.set(sessionId, null);
203
331
  return { session: session, sessionId };
204
332
  }
205
333
  }
334
+ /** Idempotently record a session's owner — never overwrites an existing owner (mirrors TiDBSessionStore.register).
335
+ * TiDB parity: register INSERTs a session_meta row so the session EXISTS (forkable) even before its
336
+ * first acquire/run — so we also ensure an EMPTY repo session exists (open if present → never overwrite history;
337
+ * create if not), else `ownerOf` would say a registered-but-never-acquired session exists while `fork` 404s it. */
206
338
  async register(sessionId, owner) {
207
339
  if (this.owners.has(sessionId))
208
- return;
340
+ return; // idempotent — never touch an already-known session's owner
209
341
  this.owners.set(sessionId, owner);
210
342
  try {
211
343
  await this.repo.open({ id: sessionId, createdAt: "" });
@@ -217,6 +349,13 @@ export class LocalSessionStore {
217
349
  throw e;
218
350
  }
219
351
  }
352
+ /** Owner of a session: `null` if registered ownerless, `undefined` if no such session (mirrors TiDBSessionStore.ownerOf).
353
+ *
354
+ * S3 修(clay 裁 2026-07-26:**local=单用户,多租户走云** ⇒ 跨重启不承诺 owner 隔离):`owners` 是纯内存
355
+ * Map,重启后失忆——曾把「失忆」答成 `undefined`(=「无此会话」),让 DELETE /v1/sessions 的 route 门短路成
356
+ * already-gone、checkpoint 的 EXISTS 镜像门 return 0 ⇒ **E21 对盘上老会话整体 no-op 且无人知道**。
357
+ * 现在 Map miss 时探 repo:盘上在 ⇒ 按 load() 两处懒回填早已写下的同一判据回填 `null`(registered
358
+ * ownerless);repo 真没有才 `undefined`。SQL 孪生无此形(owner 在 session_meta 行,重启无损)。 */
220
359
  async ownerOf(sessionId) {
221
360
  if (this.owners.has(sessionId))
222
361
  return this.owners.get(sessionId) ?? null;
@@ -231,23 +370,30 @@ export class LocalSessionStore {
231
370
  throw e;
232
371
  }
233
372
  }
373
+ /** [1196] 六轮复审:owner+leafId 原子快照——单线程 JS 内两同步/顺序读之间无 await 打断 owner 判定
374
+ * (owner 先取快照,leaf 读后 owner 复核,变了=undefined 视作删除中,调用方按删处理)。 */
234
375
  async getHead(sessionId) {
376
+ // S3 同根:Map miss ≠ 无此会话 —— 经 ownerOf 的 repo 探在腿判定(盘上在 ⇒ 回填 ownerless 后继续)。
235
377
  if (!this.owners.has(sessionId) && (await this.ownerOf(sessionId)) === undefined)
236
378
  return undefined;
237
379
  const owner = this.owners.get(sessionId) ?? null;
238
380
  const leafId = await this.getLeafId(sessionId);
239
381
  if (!this.owners.has(sessionId) || (this.owners.get(sessionId) ?? null) !== owner)
240
- return undefined;
382
+ return undefined; // 读间被删/换主
241
383
  return { owner, leafId };
242
384
  }
385
+ /** 2c P1d-β — re-stamp the service-side owner scope WITHOUT rewriting entries (the staged-commit `identical` path:
386
+ * the log is already present & equal, so only the owner needs re-stamping to the importing principal, §9). */
243
387
  restampOwner(sessionId, owner) {
244
388
  if (this.owners.has(sessionId))
245
389
  this.owners.set(sessionId, owner);
246
390
  }
247
391
  async touch(sessionId) {
248
392
  this.lastActivity.set(sessionId, new Date().toISOString());
249
- this.scheduleFlush();
393
+ this.scheduleFlush(); // persist the recency marker so the /resume picker's order survives a restart (TOC #10)
250
394
  }
395
+ /** E18 — the session's current leaf SessionTreeEntry.id (@see TiDBSessionStore.getLeafId). Reads the repo's storage
396
+ * leaf (no acquire-lock; InMemory/File repo reads don't lock). null when the session doesn't exist / has no leaf. */
251
397
  async getLeafId(sessionId) {
252
398
  try {
253
399
  const session = await this.repo.open({ id: sessionId, createdAt: "" });
@@ -259,18 +405,40 @@ export class LocalSessionStore {
259
405
  throw e;
260
406
  }
261
407
  }
408
+ /** Durable history is retained by the repo; just drop the in-flight cache entry (mirrors TiDBSessionStore.release). */
262
409
  async release(sessionId) {
263
410
  this.pending.delete(sessionId);
264
411
  }
412
+ /**
413
+ * P0.5 variant-2 (E16, clay 2026-06-26) — enumerate the caller's sessions (CC /resume picker) from the SESSION
414
+ * abstraction (`repo.list()`), keyset-paginated newest-first by last activity. Session IDENTITY comes from the
415
+ * repo (one entry per persisted session); a {@link SessionSummary} is SYNTHESIZED per id so the wire shape is
416
+ * byte-identical to {@link TiDBSessionStore.listSessions} (the run picker doesn't drift):
417
+ * - `owner` — the service-side owner map (process-local; null when unknown/ownerless). Owner FILTERING
418
+ * is best-effort here (the in-memory map IS the source of truth; a single-user local
419
+ * deployment runs REQUIRE_PRINCIPAL=off so the principal path passes owner=null and every
420
+ * null-owner session lists). The route already owner-gates at the HTTP layer.
421
+ * - `firstActivityAt` — the session's `createdAt` (repo metadata).
422
+ * - `lastActivityAt` — the touch marker (falls back to `createdAt`); the keyset sort key.
423
+ * - `objectivePreview`— the last USER-text entry, synthesized from the session tree (no run ledger).
424
+ * - `runCount`/`lastStatus` — DEGRADED (0 / ""): the session abstraction has no runs ledger; the TiDB twin
425
+ * LEFT-JOINs `task_run` for these, which the local backend (MemoryRunStore, separate
426
+ * ledger) intentionally does NOT correlate here (variant-2 is session-abstraction-only).
427
+ *
428
+ * Keyset matches the contract: STRICTLY older than the cursor, ties broken by id DESC; `limit` is honored after
429
+ * the in-memory sort. Cheap (a local single-user store holds few sessions). Errors reading one session's entries
430
+ * degrade that row's preview to null rather than failing the whole list.
431
+ */
265
432
  async listSessions(opts) {
266
- await this.ensureLoaded();
433
+ await this.ensureLoaded(); // hydrate persisted lastActivity BEFORE reading it — else a restart loses recency order (TOC #10)
267
434
  await this.ensureTitlesLoaded();
268
435
  const metas = await this.repo.list();
436
+ // Build a summary per persisted session, owner from the process-local map (degrade to null when unknown).
269
437
  const all = [];
270
438
  for (const meta of metas) {
271
439
  const owner = this.owners.has(meta.id) ? (this.owners.get(meta.id) ?? null) : null;
272
440
  if (opts.owner !== undefined && owner !== opts.owner)
273
- continue;
441
+ continue; // owner-scope (best-effort, process-local map)
274
442
  const firstActivityAt = meta.createdAt || new Date(0).toISOString();
275
443
  const lastActivityAt = this.lastActivity.get(meta.id) ?? firstActivityAt;
276
444
  all.push({
@@ -278,31 +446,43 @@ export class LocalSessionStore {
278
446
  owner,
279
447
  lastActivityAt,
280
448
  firstActivityAt,
281
- runCount: 0,
449
+ runCount: 0, // session abstraction has no runs ledger (honest degrade; TiDB twin LEFT-JOINs task_run)
282
450
  objectivePreview: await this.previewOf(meta),
283
- lastStatus: "",
284
- lastRunId: this.lastTaskRun.get(meta.id) ?? null,
285
- title: this.titleState.titles.get(meta.id) ?? null,
451
+ lastStatus: "", // no run status without the runs ledger (honest degrade — matches TiDB's null→"" mapping)
452
+ lastRunId: this.lastTaskRun.get(meta.id) ?? null, // K-5c: latest run id from the noteTaskRun seam (null if none)
453
+ title: this.titleState.titles.get(meta.id) ?? null, // auto-title
286
454
  });
287
455
  }
456
+ // `?q=` — the local twin matches the synthesized objectivePreview (the last user text; the
457
+ // SQL twins match ANY run's preview via the runs ledger — each backend searches what it durably has).
458
+ // Filter BEFORE the sort/keyset slice so pagination stays correct.
288
459
  const qlc = opts.q?.toLowerCase();
289
460
  const filtered = qlc ? all.filter((r) => r.objectivePreview?.toLowerCase().includes(qlc)) : all;
461
+ // newest-first by (lastActivityAt, sessionId) — same ORDER BY the SQL twin emits.
290
462
  filtered.sort((a, b) => a.lastActivityAt !== b.lastActivityAt
291
463
  ? (a.lastActivityAt < b.lastActivityAt ? 1 : -1)
292
464
  : (a.sessionId < b.sessionId ? 1 : -1));
465
+ // keyset: keep rows STRICTLY older than the cursor (ties by id DESC), then honor limit (mirrors the SQL WHERE).
293
466
  const c = opts.cursor;
294
467
  const after = c
295
468
  ? filtered.filter((r) => r.lastActivityAt < c.lastActivityAt || (r.lastActivityAt === c.lastActivityAt && r.sessionId < c.sessionId))
296
469
  : filtered;
297
470
  return after.slice(0, opts.limit);
298
471
  }
472
+ /** K-5c (core 1.155 SessionStore seam): core's Runner calls this at runTask START with the run's taskId, so the
473
+ * latest run surfaces as `lastRunId` on listSessions even while it is still running/suspended (the local backend
474
+ * has no runs ledger to derive it from, unlike the TiDB/PG twins). In-memory, last-write-wins per session. */
299
475
  noteTaskRun(sessionId, taskId) {
300
476
  this.lastTaskRun.set(sessionId, taskId);
301
477
  }
478
+ /** Synthesize a session's objective preview = the most-recent USER text message (the objective the run last saw).
479
+ * Reads the session's entries through the repo; any read error degrades to null (never fails the list). */
302
480
  async previewOf(meta) {
303
481
  try {
304
482
  const session = await this.repo.open({ id: meta.id, createdAt: meta.createdAt });
305
483
  const entries = await session.getEntries();
484
+ // Walk newest-first; the most recent USER message is the objective (mirrors task_run.objective_preview). A
485
+ // session with no user turn yet (e.g. registered-but-never-run) → null preview (honest empty, like the picker).
306
486
  for (let i = entries.length - 1; i >= 0; i--) {
307
487
  const text = userText(entries[i]);
308
488
  if (text)
@@ -311,9 +491,12 @@ export class LocalSessionStore {
311
491
  return null;
312
492
  }
313
493
  catch {
494
+ // best-effort: a missing/torn/unreadable session lists without a preview rather than failing the whole picker.
314
495
  return null;
315
496
  }
316
497
  }
498
+ /** E17 — fork a session's WHOLE history to a NEW id owned by `owner` (core SessionRepo.fork). Returns the new id,
499
+ * or null if the source does not exist (the route maps that to 404). */
317
500
  async fork(sourceId, owner) {
318
501
  try {
319
502
  const session = await this.repo.fork({ id: sourceId, createdAt: "" }, {});
@@ -327,60 +510,104 @@ export class LocalSessionStore {
327
510
  throw e;
328
511
  }
329
512
  }
513
+ /** 2c session-sync — EXPORT a session's full durable entry log (core `SessionRepo.exportEntries`; the
514
+ * in-memory/file repo holds the whole log so there is no floor to bypass). null if the source is unknown / the
515
+ * underlying repo lacks the optional seam. The owner is NOT exported — import re-stamps it. */
330
516
  async exportEntries(sessionId) {
331
517
  if (!this.repo.exportEntries)
332
- return null;
518
+ return null; // a repo without the optional seam can't export (older core)
333
519
  try {
334
520
  return await this.repo.exportEntries(sessionId);
335
521
  }
336
522
  catch (e) {
337
523
  if (isNotFound(e))
338
- return null;
524
+ return null; // unknown source ⇒ null (mirrors fork's 404 semantics)
339
525
  throw e;
340
526
  }
341
527
  }
528
+ /** 2c session-sync P1d-α (PULL streaming) — the IDS-ONLY projection of the full durable log (@see
529
+ * TiDBSessionStore.listEntryIds). The local repo holds the whole log in memory/file so there is no floor to bypass;
530
+ * ids are the `exportEntries` ids in order. null if the source is unknown / the repo lacks the export seam. */
342
531
  async listEntryIds(sessionId) {
343
532
  const entries = await this.exportEntries(sessionId);
344
533
  return entries === null ? null : entries.map((e) => e.id);
345
534
  }
535
+ /** 2c session-sync P1d-α (PULL streaming) — STREAM the full durable log as an async generator (@see
536
+ * TiDBSessionStore.exportEntriesStream). The LOCAL twin has no cross-instance buffering hazard (an in-memory map /
537
+ * single-process file repo), so it reads the whole log via the repo's `exportEntries` then yields it lazily, SLICED
538
+ * after `opts.afterSeq` (the LOCAL backend's native seq = the 0-based dense ARRAY INDEX of the entry in the
539
+ * oldest-first log; afterSeq=k → entries at index > k = `slice(k+1)`). afterSeq is a BACKEND-NATIVE seq cursor (a
540
+ * resume is always same-backend), so the local 0-based-index base needn't match the durable backends' `seq` column
541
+ * base. Resolves to null (NOT an empty iterable) for an unknown session — mirrors {@link exportEntries}.
542
+ * `opts.batchSize` is accepted for API parity but has no effect locally (the whole log is already in hand). */
346
543
  async exportEntriesStream(sessionId, opts) {
347
544
  const entries = await this.exportEntries(sessionId);
348
545
  if (entries === null)
349
- return null;
350
- const afterSeq = opts?.afterSeq ?? -1;
351
- const slice = entries.slice(afterSeq + 1);
546
+ return null; // unknown session — NOT an empty iterable
547
+ const afterSeq = opts?.afterSeq ?? -1; // 0-based dense → > -1 = from index 0
548
+ const slice = entries.slice(afterSeq + 1); // afterSeq=k yields entries k+1..end (seq = array index for the local repo)
352
549
  async function* gen() {
353
550
  for (const e of slice)
354
551
  yield e;
355
552
  }
356
553
  return gen();
357
554
  }
555
+ /** 2c session-sync — IMPORT a verbatim entry log into `sessionId` via core `SessionRepo.importEntries`
556
+ * (which runs the `validateEntriesForImport` fail-closed gate before writing). `owner` is the authenticated
557
+ * importing principal (re-stamped into the service-side owner map; core's local repo has no owner column, §9).
558
+ * `owner=null` (single-user local) is passed as `undefined` to the core seam (its owner param is `string | undefined`). */
358
559
  async importEntries(sessionId, owner, entries) {
359
560
  if (!this.repo.importEntries)
360
561
  throw new SessionError("storage", "session import is not supported by this local backend");
361
- await this.repo.importEntries(sessionId, owner ?? undefined, entries);
362
- this.owners.set(sessionId, owner);
363
- }
562
+ await this.repo.importEntries(sessionId, owner ?? undefined, entries); // core gate runs inside; throws invalid_session on violation
563
+ this.owners.set(sessionId, owner); // re-stamp the service-side owner scope to the importing principal
564
+ }
565
+ /** 2c session-sync (§7/§8) — IDEMPOTENT REPLACE. The local twin's replace is DELETE-then-IMPORT: core's
566
+ * InMemorySessionRepo / FileSessionRepo expose no single replace primitive, but the local backend is single-writer
567
+ * with NO compaction floor / cross-instance race (a process-local in-memory map / a single-process file repo), so
568
+ * deleteSession-then-importEntries is effectively atomic for the local case (no concurrent reader can observe the
569
+ * gap, unlike the durable cross-instance backends which do it in ONE SQL txn). Both halves are best-effort feature-
570
+ * gated: a repo lacking either seam throws the same clear error importEntries does (no silent half-write). */
364
571
  async replaceEntries(sessionId, owner, entries) {
365
572
  if (!this.repo.importEntries)
366
573
  throw new SessionError("storage", "session import is not supported by this local backend");
367
574
  if (!this.repo.delete)
368
575
  throw new SessionError("storage", "session replace is not supported by this local backend (no delete seam)");
576
+ // VALIDATE-FIRST (before the delete): the durable twins run validateEntriesForImport INSIDE a txn that ROLLs BACK on
577
+ // a bad import, so a failed import can't destroy the existing session. The local twin is delete-then-import (no
578
+ // single-writer txn), so an invalid bundle — the COMMON failure — must be rejected BEFORE deleteSession purges the
579
+ // prior session; otherwise a bad import would purge-then-fail and the existing session would be gone. (A post-delete
580
+ // fs I/O failure on importEntries remains a residual single-writer risk the durable twins don't have — noted.)
369
581
  validateEntriesForImport(entries);
370
- await this.deleteSession(sessionId, owner);
371
- await this.importEntries(sessionId, owner, entries);
372
- }
582
+ // We purge first so the subsequent import can't hit a duplicate-id collision over an existing session. delete on an
583
+ // absent session is a no-op (deleteSession swallows not_found below), so replace over a fresh session is just an
584
+ // import. importEntries re-runs the (now-known-valid) core gate before writing.
585
+ await this.deleteSession(sessionId, owner); // owner-guarded purge (defense-in-depth, single-user: owner=null matches)
586
+ await this.importEntries(sessionId, owner, entries); // core gate runs inside + re-stamps the owner scope
587
+ }
588
+ /** 2c session-sync P1d-β (PUSH streaming) — begin a STAGED import. The LOCAL backend is single-writer with no
589
+ * cross-instance / cross-replica concurrency story (the durable twins carry that), so the staging handle simply
590
+ * ACCUMULATES the streamed batches in memory then, on commit, runs the SAME validate-first → replaceEntries the
591
+ * whole-bundle local import uses (effectively atomic locally — no concurrent reader can observe the gap). Keep it
592
+ * simple per §1: the durable twins are where the SHADOW-id swap + bounded-memory + concurrency matter. */
373
593
  beginImportStaging(realSessionId, token) {
374
594
  const h = new LocalStagingHandle(this, realSessionId, stagingIdFor(realSessionId, token));
375
595
  this.stagings.set(h.stagingId, h);
376
596
  return h;
377
597
  }
598
+ /** 2c P1d-β (staged-row inspection) — the in-memory staged entries for a local staging id. No longer the import gate
599
+ * (the per-line StreamingImportValidator validates during the Phase-B stream); a tests/diagnostics seam. [] for an
600
+ * unknown/finished staging id (mirrors the durable twins' empty-set return). */
378
601
  async readStagedEntries(stagingId) {
379
602
  return this.stagings.get(stagingId)?.snapshotEntries() ?? [];
380
603
  }
604
+ /** Drop a finished/aborted local staging handle from the registry (called by the handle on commit/abort). */
381
605
  forgetStaging(stagingId) {
382
606
  this.stagings.delete(stagingId);
383
607
  }
608
+ /** E21 — purge a session's conversation history (core SessionRepo.delete), owner-guarded (single-user: owner=null
609
+ * matches). Idempotent: a missing/wrong-owner session is a no-op returning false. The runs-ledger / checkpoint /
610
+ * tool-result rows are purged separately by the route's purgeSession coordinator. */
384
611
  async deleteSession(sessionId, owner) {
385
612
  const known = this.owners.has(sessionId);
386
613
  if (!known || !ownerEq(this.owners.get(sessionId) ?? null, owner))
@@ -394,30 +621,50 @@ export class LocalSessionStore {
394
621
  }
395
622
  this.owners.delete(sessionId);
396
623
  this.lastActivity.delete(sessionId);
397
- this.lastTaskRun.delete(sessionId);
624
+ this.lastTaskRun.delete(sessionId); // K-5c (review): drop the run anchor too — else the map grows on delete churn
625
+ // and a re-acquire(sameId) would surface the PRIOR incarnation's lastRunId (stale anchor).
626
+ // Drop the auto-title too (E21 parity with the SQL twins' session_meta row delete) and
627
+ // persist the removal through the SAME serialized chain (an in-flight grant snapshot must not rename
628
+ // AFTER this removal and resurrect it — the chain re-snapshots the map, so the delete always wins).
629
+ // 🔴 hydrate FIRST (workflow audit 2026-07-13): after a restart the map starts empty — an un-hydrated
630
+ // delete() returns false and the sidecar keeps the title (an E21 right-to-delete residue that would
631
+ // RESURRECT onto a same-id re-import). ensureTitlesLoaded is memoized — first caller pays, rest no-op.
398
632
  await this.ensureTitlesLoaded();
399
633
  if (this.titleState.titles.delete(sessionId))
400
634
  await this.persistTitles();
401
- this.scheduleFlush();
635
+ this.scheduleFlush(); // persist the removal so a deleted session doesn't linger in the sidecar across a restart
402
636
  return true;
403
637
  }
404
638
  get size() {
405
639
  return this.pending.size;
406
640
  }
641
+ /** Graceful shutdown: AWAIT the final sidecar write (bug C) so a restart doesn't lose the last debounced-but-unwritten
642
+ * touch. Cancels any queued debounce, then awaits a flush (which chains after any in-flight write and merges disk). */
407
643
  async dispose() {
408
644
  this.pending.clear();
409
645
  if (this.flushTimer) {
410
646
  clearTimeout(this.flushTimer);
411
647
  this.flushTimer = undefined;
412
648
  }
649
+ // Always do a final flush when persisting (a pending debounce OR an in-flight write may hold the last touch); when
650
+ // not persisting it's a cheap no-op. AWAIT it (and the chain) so close() can't release the data-dir lock first.
413
651
  if (this.sidecarPath)
414
652
  await this.flush();
415
653
  }
416
654
  }
655
+ /**
656
+ * 2c session-sync P1d-β — the LOCAL {@link StagingHandle}. Unlike the durable twins (which stage into a shadow
657
+ * session_event partition + atomically rename it), the local backend is single-writer with no cross-instance race, so
658
+ * this handle ACCUMULATES the streamed batches in memory (idempotent on entry id — a re-streamed batch is deduped) and,
659
+ * on commit, runs the SAME validate-first → replaceEntries the whole-bundle local import uses (the existing
660
+ * delete-then-import that is effectively atomic for the single-process local case). It still re-classifies AT COMMIT
661
+ * (against the dst's current ids) so an unconsented fork/stale is refused, matching the durable twins' contract.
662
+ */
417
663
  class LocalStagingHandle {
418
664
  store;
419
665
  realSessionId;
420
666
  stagingId;
667
+ /** Accumulated staged entries, deduped by entry id (resume re-streams from the start → dedup keeps it idempotent). */
421
668
  staged = [];
422
669
  seen = new Set();
423
670
  constructor(store, realSessionId, stagingId) {
@@ -428,30 +675,38 @@ class LocalStagingHandle {
428
675
  async appendBatch(entries) {
429
676
  for (const e of entries) {
430
677
  if (this.seen.has(e.id))
431
- continue;
678
+ continue; // idempotent (resume re-stream) — dedup by entry id
432
679
  this.seen.add(e.id);
433
680
  this.staged.push(e);
434
681
  }
435
682
  }
683
+ /** The staged entries, oldest-first (readStagedEntries reads these). A snapshot copy (callers don't mutate). */
436
684
  snapshotEntries() {
437
685
  return [...this.staged];
438
686
  }
687
+ /** Commit = in-commit re-classify (dst current ids vs staged) → refuse an unconsented fork/stale → validate + the
688
+ * idempotent replaceEntries (validate-first-then-delete-then-import, effectively atomic locally). */
439
689
  async commit(owner, opts) {
440
690
  const dstEntries = await this.store.exportEntries(this.realSessionId);
441
691
  const rel = classifySyncRelationshipByIds(this.staged.map((e) => e.id), dstEntries === null ? null : dstEntries.map((e) => e.id));
442
692
  if ((rel.relation === "fork" || rel.relation === "stale") && opts?.resolution !== "overwrite-dst") {
443
693
  throw new SyncConflictError(this.realSessionId, rel);
444
694
  }
695
+ // 🔴 2026-07-26:`identical` 不再等于"内容相同"。id 是 uuidv7、不是内容寻址 ⇒ 同 id 异载荷完全可能,
696
+ // 而 commit 这里**手上同时有两份日志**,所以内容比较不需要调用方配合(见 `session-sync-content.ts` 顶注:
697
+ // 我在黑板 [1701] 说过"server 单方面修不了",那句只对 Phase A 成立,对这一半是错的)。
698
+ // 判据不符 ⇒ **照常改写**(不抛错、不 409 —— core 定的口径是"不符只意味着多干一趟活")。
445
699
  const contentEqual = rel.relation === "identical" && identicalIdsAlsoIdenticalContent(this.staged, dstEntries);
446
700
  if (rel.relation !== "identical" || !contentEqual) {
447
- await this.store.replaceEntries(this.realSessionId, owner, this.staged);
701
+ await this.store.replaceEntries(this.realSessionId, owner, this.staged); // validate-first + delete-then-import (atomic locally)
448
702
  }
449
703
  else {
450
- this.store.restampOwner(this.realSessionId, owner);
704
+ this.store.restampOwner(this.realSessionId, owner); // 内容也相同 ⇒ 只重戳 owner(此时跳过写入是对的)
451
705
  }
452
706
  this.store.forgetStaging(this.stagingId);
453
707
  return { relation: rel.relation };
454
708
  }
709
+ /** Abort = drop the in-memory staged buffer (no durable rows were ever written locally) + forget the handle. */
455
710
  async abort() {
456
711
  this.staged.length = 0;
457
712
  this.seen.clear();