@wrongstack/core 0.313.0 → 0.316.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/legacy-journal-import.d.ts +0 -6
  3. package/dist/chronicle/metrics-ingest.d.ts +14 -0
  4. package/dist/chronicle/metrics-schema.d.ts +10 -1
  5. package/dist/chronicle/metrics-store.d.ts +20 -2
  6. package/dist/chronicle/project-server.js +98 -4874
  7. package/dist/chronicle/sink.d.ts +13 -0
  8. package/dist/chronicle/sqlite-journal.d.ts +20 -2
  9. package/dist/chunk-25LSLC3G.js +334 -0
  10. package/dist/chunk-27QIURLN.js +1073 -0
  11. package/dist/chunk-2FK4WTFF.js +207 -0
  12. package/dist/chunk-2YU4WVBW.js +134 -0
  13. package/dist/chunk-3AGWVSS7.js +4608 -0
  14. package/dist/chunk-3BDV7RL3.js +211 -0
  15. package/dist/chunk-3ECENVHL.js +325 -0
  16. package/dist/chunk-3XPVB7BA.js +135 -0
  17. package/dist/chunk-45WI72ZB.js +16884 -0
  18. package/dist/chunk-4BMVPOHO.js +3748 -0
  19. package/dist/chunk-4I7HGVVA.js +363 -0
  20. package/dist/chunk-4LCZVJVF.js +317 -0
  21. package/dist/chunk-4SJNS7OB.js +217 -0
  22. package/dist/chunk-4VWO3ZXL.js +342 -0
  23. package/dist/chunk-57GEWK5X.js +26 -0
  24. package/dist/chunk-5BMIE7QG.js +46 -0
  25. package/dist/chunk-5S437PRC.js +50 -0
  26. package/dist/chunk-5XDUGTKN.js +511 -0
  27. package/dist/chunk-5YPSXQXM.js +1443 -0
  28. package/dist/chunk-6FHWXLPT.js +10 -0
  29. package/dist/chunk-6H5GEOET.js +2406 -0
  30. package/dist/chunk-6NWEVFEL.js +151 -0
  31. package/dist/chunk-AYW5YINX.js +58 -0
  32. package/dist/chunk-B4AMLZPB.js +9 -0
  33. package/dist/chunk-B7F4WVP4.js +65 -0
  34. package/dist/chunk-BJHVULHO.js +55 -0
  35. package/dist/chunk-BPBKKBUK.js +2085 -0
  36. package/dist/chunk-BYD345OX.js +589 -0
  37. package/dist/chunk-C27KDLOX.js +279 -0
  38. package/dist/chunk-CU546ESW.js +597 -0
  39. package/dist/chunk-DLHHXUAM.js +172 -0
  40. package/dist/chunk-DOTHQSJ2.js +429 -0
  41. package/dist/chunk-DYLPMV4Q.js +711 -0
  42. package/dist/chunk-EODDVJBM.js +82 -0
  43. package/dist/chunk-F3JWNST7.js +361 -0
  44. package/dist/chunk-FC2ZGRF3.js +140 -0
  45. package/dist/chunk-FCBTO76Z.js +5786 -0
  46. package/dist/chunk-FHZVFYSL.js +396 -0
  47. package/dist/chunk-FNXVT7HF.js +52 -0
  48. package/dist/chunk-FWNLEZYG.js +259 -0
  49. package/dist/chunk-FXQLIX3K.js +211 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GDQ7CZRQ.js +116 -0
  52. package/dist/chunk-GJJGJB2R.js +1374 -0
  53. package/dist/chunk-GTCU6DZ3.js +1 -0
  54. package/dist/chunk-HA4ZYA6T.js +178 -0
  55. package/dist/chunk-HI3JGZR2.js +188 -0
  56. package/dist/chunk-HMLIGAU7.js +395 -0
  57. package/dist/chunk-HXKZ7OBQ.js +303 -0
  58. package/dist/chunk-I7DALG7J.js +292 -0
  59. package/dist/chunk-IRUTEHCE.js +960 -0
  60. package/dist/chunk-IYFBFX34.js +86 -0
  61. package/dist/chunk-J2WRWR37.js +1104 -0
  62. package/dist/chunk-JFPJ6LLQ.js +129 -0
  63. package/dist/chunk-JMULFFLN.js +9 -0
  64. package/dist/chunk-JO4MGB6Z.js +1264 -0
  65. package/dist/chunk-JVKB2TBL.js +48 -0
  66. package/dist/chunk-JX7VBZDK.js +31 -0
  67. package/dist/chunk-K6BXGITA.js +931 -0
  68. package/dist/chunk-KDQJCT2J.js +45 -0
  69. package/dist/chunk-KJ2ZTQSL.js +47 -0
  70. package/dist/chunk-KZMSZV32.js +187 -0
  71. package/dist/chunk-LA7CLDRW.js +129 -0
  72. package/dist/chunk-LHOA5DWA.js +28 -0
  73. package/dist/chunk-LSMMOS2Z.js +119 -0
  74. package/dist/chunk-LX5QWLBL.js +27 -0
  75. package/dist/chunk-LXWCMQNJ.js +789 -0
  76. package/dist/chunk-M27LIEKV.js +2829 -0
  77. package/dist/chunk-MJPHJRAL.js +663 -0
  78. package/dist/chunk-MTL4O5YR.js +228 -0
  79. package/dist/chunk-NDBIPFNY.js +102 -0
  80. package/dist/chunk-NEMIWEXC.js +1199 -0
  81. package/dist/chunk-NHJ75HXY.js +1 -0
  82. package/dist/chunk-NIOAGFPB.js +1510 -0
  83. package/dist/chunk-NS52ISWS.js +355 -0
  84. package/dist/chunk-NSOFPKGT.js +5352 -0
  85. package/dist/chunk-NVIZG63R.js +757 -0
  86. package/dist/chunk-O37BQ65S.js +21 -0
  87. package/dist/chunk-OOYZPOXM.js +308 -0
  88. package/dist/chunk-OSGLYW5J.js +28 -0
  89. package/dist/chunk-PFCMSSUF.js +1640 -0
  90. package/dist/chunk-PFZO3C6H.js +14 -0
  91. package/dist/chunk-PGRVXIMY.js +158 -0
  92. package/dist/chunk-PHH7LD45.js +113 -0
  93. package/dist/chunk-PISBFBJN.js +109 -0
  94. package/dist/chunk-POOOIWE5.js +9 -0
  95. package/dist/chunk-PQNS3U7Z.js +1240 -0
  96. package/dist/chunk-QSTSJOCK.js +402 -0
  97. package/dist/chunk-RFQMOKWK.js +282 -0
  98. package/dist/chunk-RJIZEJ42.js +30 -0
  99. package/dist/chunk-RTOTNK3P.js +484 -0
  100. package/dist/chunk-RXKSDUAU.js +1172 -0
  101. package/dist/chunk-S45JPKQN.js +3317 -0
  102. package/dist/chunk-SAFHACFA.js +152 -0
  103. package/dist/chunk-SQJVFEBV.js +41 -0
  104. package/dist/chunk-SVT4RNTR.js +49 -0
  105. package/dist/chunk-SWUFN7XM.js +43 -0
  106. package/dist/chunk-SXMVYC5H.js +174 -0
  107. package/dist/chunk-SZ6AYUIZ.js +23 -0
  108. package/dist/chunk-TBRL2RCC.js +9 -0
  109. package/dist/chunk-TCCHINEF.js +655 -0
  110. package/dist/chunk-THEGTRAG.js +316 -0
  111. package/dist/chunk-TR4LQE7L.js +2256 -0
  112. package/dist/chunk-TRT2HRWZ.js +18 -0
  113. package/dist/chunk-TWJMGXCI.js +900 -0
  114. package/dist/chunk-U45AXT5D.js +1023 -0
  115. package/dist/chunk-U4FWBSO3.js +509 -0
  116. package/dist/chunk-UBGDX6FI.js +147 -0
  117. package/dist/chunk-UJILKYES.js +75 -0
  118. package/dist/chunk-ULMAQV3T.js +234 -0
  119. package/dist/chunk-VVBP6KGT.js +9 -0
  120. package/dist/chunk-W5VITDM2.js +168 -0
  121. package/dist/chunk-WAHQVNWT.js +194 -0
  122. package/dist/chunk-WBPVKEEE.js +227 -0
  123. package/dist/chunk-WGGYRBXH.js +5434 -0
  124. package/dist/chunk-WLYKU45P.js +5770 -0
  125. package/dist/chunk-WP7BVANJ.js +53 -0
  126. package/dist/chunk-WQQON3SV.js +473 -0
  127. package/dist/chunk-WQTOVMGV.js +119 -0
  128. package/dist/chunk-XBDP4QPW.js +7 -0
  129. package/dist/chunk-XEBHSJLT.js +103 -0
  130. package/dist/chunk-XF4VUMKW.js +231 -0
  131. package/dist/chunk-XQ2ECNK3.js +1 -0
  132. package/dist/chunk-Y5YXDM7D.js +74 -0
  133. package/dist/chunk-YOPAUVU4.js +2588 -0
  134. package/dist/chunk-YPT4A46F.js +362 -0
  135. package/dist/chunk-YWXT3JNM.js +425 -0
  136. package/dist/chunk-YY6APYUM.js +49 -0
  137. package/dist/chunk-Z2IJUO45.js +691 -0
  138. package/dist/chunk-ZHZ2Y2DH.js +1643 -0
  139. package/dist/chunk-ZT7JNIGX.js +152 -0
  140. package/dist/coordination/agent-monitor.d.ts +9 -1
  141. package/dist/coordination/agent-subagent-runner.d.ts +1 -1
  142. package/dist/coordination/agents/agent-prompts.d.ts +8 -0
  143. package/dist/coordination/agents/index.d.ts +33 -1
  144. package/dist/coordination/agents/index.js +184 -6269
  145. package/dist/coordination/agents/phase3-wave1-platform.d.ts +1 -3
  146. package/dist/coordination/agents/phase3-wave2-meta.d.ts +1 -3
  147. package/dist/coordination/agents/phase8-wave3-products.d.ts +1 -3
  148. package/dist/coordination/agents/phase9-wave4-platform-meta.d.ts +1 -3
  149. package/dist/coordination/agents/project-agent-identity.d.ts +11 -11
  150. package/dist/coordination/agents/types.d.ts +13 -2
  151. package/dist/coordination/brain-monitor.d.ts +30 -7
  152. package/dist/coordination/collab-debug.d.ts +1 -1
  153. package/dist/coordination/collab-pause.d.ts +1 -1
  154. package/dist/coordination/delegate-tool.d.ts +7 -5
  155. package/dist/coordination/director-kanban-queue-helpers.d.ts +1 -0
  156. package/dist/coordination/director-options.d.ts +7 -0
  157. package/dist/coordination/director-spawn-model.d.ts +17 -0
  158. package/dist/coordination/director-tools.d.ts +2 -2
  159. package/dist/coordination/director.d.ts +12 -0
  160. package/dist/coordination/dispatcher.d.ts +8 -0
  161. package/dist/coordination/fleet-bus.d.ts +35 -4
  162. package/dist/coordination/fleet-supervisor.d.ts +10 -2
  163. package/dist/coordination/fleet.d.ts +16 -7
  164. package/dist/coordination/index.d.ts +4 -0
  165. package/dist/coordination/index.js +402 -34324
  166. package/dist/coordination/kanban-dispatch-port.d.ts +2 -2
  167. package/dist/coordination/mailbox-http-router.d.ts +4 -3
  168. package/dist/coordination/mailbox-http-validation.d.ts +1 -1
  169. package/dist/coordination/mailbox-project-server.js +48 -3276
  170. package/dist/coordination/mailbox-types.d.ts +24 -6
  171. package/dist/coordination/model-tier-leader.d.ts +122 -0
  172. package/dist/coordination/model-tier.d.ts +122 -0
  173. package/dist/coordination/multi-agent-coordinator.d.ts +24 -0
  174. package/dist/coordination/origin-session.d.ts +16 -0
  175. package/dist/coordination/provider-status-tracker.d.ts +41 -5
  176. package/dist/coordination/session-note-hub.d.ts +44 -0
  177. package/dist/coordination/session-note-tool.d.ts +10 -0
  178. package/dist/core/agent.d.ts +15 -2
  179. package/dist/core/context.d.ts +22 -3
  180. package/dist/core/index.d.ts +2 -0
  181. package/dist/core/index.js +239 -12360
  182. package/dist/core/model-ref.js +5 -23
  183. package/dist/core/provider-runner.d.ts +36 -1
  184. package/dist/core/request-conversation-binding.d.ts +37 -0
  185. package/dist/core/session-notes.d.ts +25 -0
  186. package/dist/core/system-prompt-builder.d.ts +11 -2
  187. package/dist/core/system-prompt-variants.d.ts +78 -0
  188. package/dist/design/index.js +50 -1087
  189. package/dist/execution/auto-compaction-middleware.d.ts +11 -0
  190. package/dist/execution/autonomy-brain.d.ts +2 -1
  191. package/dist/execution/autonomy-prompt-contributor.d.ts +7 -2
  192. package/dist/execution/compaction-core.d.ts +4 -4
  193. package/dist/execution/eternal-autonomy.d.ts +1 -1
  194. package/dist/execution/index.d.ts +1 -1
  195. package/dist/execution/index.js +1615 -21895
  196. package/dist/execution/model-runtime.d.ts +0 -7
  197. package/dist/execution/parallel-eternal-engine.d.ts +7 -7
  198. package/dist/execution/prompt-enhancer.js +16 -535
  199. package/dist/execution/retry-policy.d.ts +31 -1
  200. package/dist/extension/index.js +7 -311
  201. package/dist/goal/index.js +33 -3435
  202. package/dist/hooks/index.js +14 -1138
  203. package/dist/hq/index.js +114 -5139
  204. package/dist/hq/persistence/event-log.d.ts +0 -1
  205. package/dist/hq/persistence.d.ts +4 -4
  206. package/dist/hq/protocol.js +21 -668
  207. package/dist/hq/publisher.d.ts +2 -0
  208. package/dist/index.d.ts +14 -11
  209. package/dist/index.js +1502 -92655
  210. package/dist/infrastructure/index.d.ts +4 -4
  211. package/dist/infrastructure/index.js +504 -5230
  212. package/dist/infrastructure/token-counter.d.ts +9 -0
  213. package/dist/kernel/events/agent-events.d.ts +77 -3
  214. package/dist/kernel/events/brain-events.d.ts +12 -0
  215. package/dist/kernel/events/provider-events.d.ts +22 -1
  216. package/dist/kernel/events/session-events.d.ts +14 -0
  217. package/dist/kernel/events/tool-events.d.ts +13 -0
  218. package/dist/kernel/index.js +14 -992
  219. package/dist/mailbox-attach.d.ts +1 -1
  220. package/dist/models/index.js +41 -1117
  221. package/dist/notifications/index.js +3 -108
  222. package/dist/observability/event-bridge.d.ts +25 -5
  223. package/dist/observability/index.d.ts +9 -8
  224. package/dist/observability/index.js +477 -631
  225. package/dist/observability/tool-usage-source.d.ts +65 -0
  226. package/dist/plugin/index.d.ts +5 -0
  227. package/dist/plugin/index.js +600 -14835
  228. package/dist/plugins/auto-review-plugin.d.ts +3 -2
  229. package/dist/plugins/review-finding-commands.d.ts +0 -8
  230. package/dist/plugins/review-finding-store.js +22 -0
  231. package/dist/plugins/review-finding-types.js +19 -0
  232. package/dist/plugins/review-report-integration.js +21 -0
  233. package/dist/plugins/review-report-store.js +20 -0
  234. package/dist/plugins/review-report-types.js +13 -0
  235. package/dist/prompts/index.js +14 -948
  236. package/dist/registry/index.js +9 -822
  237. package/dist/registry/tool-registry.d.ts +59 -9
  238. package/dist/registry-7N4O3E7W.js +14 -0
  239. package/dist/replay/index.js +6 -84
  240. package/dist/security/capabilities.d.ts +6 -0
  241. package/dist/security/file-permissions.d.ts +1 -1
  242. package/dist/security/index.js +161 -2235
  243. package/dist/security/permission-policy.d.ts +12 -1
  244. package/dist/security/secret-scrubber.d.ts +15 -0
  245. package/dist/session-catalog/index.d.ts +1 -0
  246. package/dist/session-catalog/index.js +41 -2103
  247. package/dist/session-catalog/project-server.js +59 -1435
  248. package/dist/session-catalog/protocol.d.ts +32 -1
  249. package/dist/session-catalog/registry.d.ts +17 -1
  250. package/dist/session-catalog/session-agents.d.ts +56 -0
  251. package/dist/session-catalog/store-rebuild.d.ts +14 -0
  252. package/dist/session-catalog/store-schema.d.ts +26 -0
  253. package/dist/session-catalog/store.d.ts +27 -1
  254. package/dist/session-note-attach.d.ts +9 -0
  255. package/dist/skills/index.js +35 -1578
  256. package/dist/statusline/index.d.ts +43 -0
  257. package/dist/statusline/index.js +102 -0
  258. package/dist/storage/board-store-port.d.ts +2 -2
  259. package/dist/storage/config-loader/in-project-policy.d.ts +29 -0
  260. package/dist/storage/config-loader.d.ts +3 -2
  261. package/dist/storage/file-session-writer.d.ts +4 -2
  262. package/dist/storage/goal-coordination.d.ts +6 -1
  263. package/dist/storage/goal-kanban.d.ts +4 -1
  264. package/dist/storage/index.d.ts +12 -4
  265. package/dist/storage/index.js +198 -15601
  266. package/dist/storage/input-history-store.d.ts +2 -4
  267. package/dist/storage/memory-curator.d.ts +133 -0
  268. package/dist/storage/session-agent-attribution.d.ts +39 -0
  269. package/dist/storage/session-doctor.d.ts +109 -0
  270. package/dist/storage/session-event-bridge.d.ts +5 -7
  271. package/dist/storage/session-outcome.d.ts +42 -0
  272. package/dist/storage/session-recovery.d.ts +55 -0
  273. package/dist/storage/session-store/load-session-data.d.ts +11 -0
  274. package/dist/storage/session-store/prune-helpers.d.ts +1 -1
  275. package/dist/storage/session-store/resume-session.d.ts +0 -2
  276. package/dist/storage/session-store/types.d.ts +8 -0
  277. package/dist/storage/session-store.d.ts +3 -3
  278. package/dist/storage/session-summary-tracker.d.ts +8 -1
  279. package/dist/storage/session-write-buffer.d.ts +18 -0
  280. package/dist/storage/session-writer-truncate.d.ts +2 -0
  281. package/dist/tasking/index.js +10 -676
  282. package/dist/tools/fallback-manage-tools.d.ts +6 -4
  283. package/dist/tools/index.js +52 -9590
  284. package/dist/tools/model-tier-set-tool.d.ts +66 -0
  285. package/dist/types/agent-bridge.d.ts +2 -1
  286. package/dist/types/config/autonomy.d.ts +5 -0
  287. package/dist/types/config/mcp-features.d.ts +7 -0
  288. package/dist/types/config/model-tiers.d.ts +148 -0
  289. package/dist/types/config/root.d.ts +9 -0
  290. package/dist/types/config/tools.d.ts +73 -0
  291. package/dist/types/config/ui.d.ts +1 -1
  292. package/dist/types/config.d.ts +1 -0
  293. package/dist/types/context.d.ts +2 -0
  294. package/dist/types/default-config.d.ts +6 -0
  295. package/dist/types/errors.d.ts +20 -1
  296. package/dist/types/index.d.ts +11 -9
  297. package/dist/types/index.js +131 -1342
  298. package/dist/types/multi-agent.d.ts +43 -0
  299. package/dist/types/plugin.d.ts +1 -1
  300. package/dist/types/provider-runner.d.ts +8 -0
  301. package/dist/types/provider.d.ts +15 -0
  302. package/dist/types/runtime-capability-manifest.d.ts +1 -1
  303. package/dist/types/secret-scrubber.d.ts +15 -0
  304. package/dist/types/session-markers.d.ts +37 -10
  305. package/dist/types/session-markers.js +8 -144
  306. package/dist/types/session-timeline.d.ts +197 -0
  307. package/dist/types/session-timeline.js +12 -0
  308. package/dist/types/session.d.ts +172 -11
  309. package/dist/types/system-prompt.d.ts +27 -0
  310. package/dist/utils/config-backup.d.ts +9 -1
  311. package/dist/utils/error.js +3 -4
  312. package/dist/utils/expect-defined.js +3 -9
  313. package/dist/utils/heap-watchdog.js +6 -706
  314. package/dist/utils/index.js +271 -5685
  315. package/dist/utils/message-invariants.d.ts +4 -1
  316. package/dist/utils/regex-guard.d.ts +1 -1
  317. package/dist/utils/sage-output-block.js +5 -42
  318. package/dist/utils/socket-path.d.ts +1 -1
  319. package/dist/utils/token-estimate.d.ts +6 -2
  320. package/dist/utils/wstack-paths.d.ts +2 -0
  321. package/dist/wiring/proxy-rewrite.d.ts +46 -0
  322. package/dist/wiring/proxy-rewrite.js +24 -121
  323. package/dist/worktree/index.js +6 -887
  324. package/instructions/agents/explore-companion.md +45 -18
  325. package/instructions/agents/memory-curator.md +34 -22
  326. package/instructions/coordination/director-preamble.md +12 -7
  327. package/instructions/coordination/subagent-baseline.md +26 -5
  328. package/instructions/leader-after-task.md +4 -1
  329. package/instructions/llm/chimera-review.md +30 -9
  330. package/instructions/llm/memory-curator.md +31 -0
  331. package/instructions/sections/tool/delegation-compact.md +2 -0
  332. package/instructions/sections/tool/delegation-full.md +5 -1
  333. package/instructions/sections/tool/mailbox-compact.md +5 -1
  334. package/instructions/sections/tool/mailbox-full.md +3 -0
  335. package/instructions/sections/tool/session-note-compact.md +3 -0
  336. package/instructions/sections/tool/session-note-full.md +15 -0
  337. package/instructions/system-lite.md +9 -4
  338. package/instructions/system-pro.md +66 -143
  339. package/instructions/system.md +73 -175
  340. package/package.json +32 -4
  341. package/dist/coordination/agents.d.ts +0 -8
  342. package/dist/coordination/transport.d.ts +0 -2
  343. package/dist/storage/session-writer/session-writer-summary-tracker.d.ts +0 -28
@@ -0,0 +1,1073 @@
1
+ import {
2
+ shouldSkipSessionDirectoryEntry
3
+ } from "./chunk-JMULFFLN.js";
4
+ import {
5
+ SESSION_CATALOG_DEFAULT_LEASE_MS,
6
+ SESSION_CATALOG_DEFAULT_RESERVATION_MS,
7
+ SESSION_CATALOG_MAX_AGENTS
8
+ } from "./chunk-AYW5YINX.js";
9
+ import {
10
+ isSessionTranscriptFileName
11
+ } from "./chunk-5BMIE7QG.js";
12
+ import {
13
+ loadDatabaseSync
14
+ } from "./chunk-IRUTEHCE.js";
15
+ import {
16
+ DefaultSecretScrubber
17
+ } from "./chunk-QSTSJOCK.js";
18
+ import {
19
+ isPidAlive
20
+ } from "./chunk-TRT2HRWZ.js";
21
+ import {
22
+ atomicWrite
23
+ } from "./chunk-OSGLYW5J.js";
24
+ import {
25
+ totalUsageTokens
26
+ } from "./chunk-3ECENVHL.js";
27
+
28
+ // src/session-catalog/session-agents.ts
29
+ function deriveSessionAgents(events) {
30
+ const rows = /* @__PURE__ */ new Map();
31
+ const upsert = (agentId) => {
32
+ let row = rows.get(agentId);
33
+ if (!row) {
34
+ row = { agentId, status: "running", interleavedEventCount: 0 };
35
+ rows.set(agentId, row);
36
+ }
37
+ return row;
38
+ };
39
+ for (const event of events) {
40
+ if (event.agentId !== void 0 && event.agentId !== "") {
41
+ const isLifecycle = event.type === "agent_spawned" || event.type === "agent_session_linked" || event.type === "agent_stopped" || event.type === "agent_error";
42
+ const row = upsert(event.agentId);
43
+ if (!isLifecycle) row.interleavedEventCount += 1;
44
+ }
45
+ switch (event.type) {
46
+ case "agent_spawned": {
47
+ const row = upsert(event.agentId);
48
+ row.role = event.role;
49
+ row.spawnedAt ??= event.ts;
50
+ break;
51
+ }
52
+ case "agent_session_linked": {
53
+ const row = upsert(event.agentId);
54
+ row.agentSessionId = event.agentSessionId;
55
+ if (event.transcriptPath !== void 0) row.transcriptPath = event.transcriptPath;
56
+ if (event.provider !== void 0) row.provider = event.provider;
57
+ if (event.model !== void 0) row.model = event.model;
58
+ if (event.parentAgentId !== void 0) row.parentAgentId = event.parentAgentId;
59
+ row.spawnedAt ??= event.ts;
60
+ break;
61
+ }
62
+ case "agent_stopped": {
63
+ const row = upsert(event.agentId);
64
+ row.endedAt = event.ts;
65
+ if (row.status !== "failed") {
66
+ row.status = event.reason === "failed" || event.reason === "aborted" ? "failed" : "completed";
67
+ }
68
+ if (event.usage !== void 0) row.usage = event.usage;
69
+ break;
70
+ }
71
+ case "agent_error": {
72
+ const row = upsert(event.agentId);
73
+ row.status = "failed";
74
+ row.error = event.error;
75
+ row.endedAt ??= event.ts;
76
+ break;
77
+ }
78
+ default:
79
+ break;
80
+ }
81
+ }
82
+ return [...rows.values()];
83
+ }
84
+
85
+ // src/session-catalog/store.ts
86
+ import { randomBytes, randomUUID } from "node:crypto";
87
+ import * as fs2 from "node:fs";
88
+ import * as path2 from "node:path";
89
+
90
+ // src/session-catalog/store-rebuild.ts
91
+ import * as fs from "node:fs";
92
+ import * as path from "node:path";
93
+
94
+ // src/session-catalog/store-schema.ts
95
+ import { createHash, timingSafeEqual } from "node:crypto";
96
+ var SCHEMA_VERSION = 1;
97
+ var MAX_LEASE_MS = 12e4;
98
+ var MAX_RESERVATION_MS = 6e4;
99
+ var MAX_MAINTENANCE_MS = 5 * 6e4;
100
+ var MAX_PAGE = 1e3;
101
+ function hashSecret(secret) {
102
+ return createHash("sha256").update(secret).digest("hex");
103
+ }
104
+ function secretMatches(secret, expectedHex) {
105
+ const actual = Buffer.from(hashSecret(secret), "hex");
106
+ const expected = Buffer.from(expectedHex, "hex");
107
+ return actual.length === expected.length && timingSafeEqual(actual, expected);
108
+ }
109
+ function boundedMs(value, fallback, max) {
110
+ return Math.min(max, Math.max(1e3, Number.isFinite(value) ? Math.floor(value) : fallback));
111
+ }
112
+ function parseJson(value) {
113
+ return JSON.parse(value);
114
+ }
115
+ function conflict(message) {
116
+ const error = new Error(message);
117
+ error.name = "SessionOwnershipConflictError";
118
+ return error;
119
+ }
120
+ function assertId(value, label = "session id") {
121
+ if (!value || value.length > 256 || value.includes("\\") || value.startsWith("/") || value.includes("..")) {
122
+ throw new TypeError(`Invalid ${label}`);
123
+ }
124
+ }
125
+ function boundPresenceValue(value, depth) {
126
+ if (typeof value === "string") {
127
+ return value.length <= 6e3 ? value : `${value.slice(0, 5988)}\u2026[truncated]`;
128
+ }
129
+ if (value === null || typeof value !== "object") return value;
130
+ if (depth >= 8) return "[truncated depth]";
131
+ if (Array.isArray(value)) {
132
+ return value.slice(0, 64).map((item) => boundPresenceValue(item, depth + 1));
133
+ }
134
+ const result = {};
135
+ for (const [key, item] of Object.entries(value).slice(0, 64)) {
136
+ result[key] = boundPresenceValue(item, depth + 1);
137
+ }
138
+ return result;
139
+ }
140
+ function configureCatalogDatabase(db) {
141
+ db.exec(
142
+ "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000; PRAGMA synchronous=NORMAL;"
143
+ );
144
+ }
145
+ function initializeCatalogSchema(db) {
146
+ db.exec(`
147
+ CREATE TABLE IF NOT EXISTS catalog_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL);
148
+ CREATE TABLE IF NOT EXISTS sessions (
149
+ session_id TEXT PRIMARY KEY,
150
+ transcript_relative_path TEXT NOT NULL,
151
+ summary_relative_path TEXT NOT NULL,
152
+ summary_json TEXT NOT NULL,
153
+ transcript_size INTEGER NOT NULL DEFAULT 0,
154
+ transcript_mtime_ms REAL NOT NULL DEFAULT 0,
155
+ summary_revision INTEGER NOT NULL DEFAULT 1,
156
+ indexed_at TEXT NOT NULL,
157
+ damaged INTEGER NOT NULL DEFAULT 0
158
+ );
159
+ CREATE TABLE IF NOT EXISTS session_leases (
160
+ session_id TEXT PRIMARY KEY,
161
+ lease_id TEXT NOT NULL UNIQUE,
162
+ lease_secret_hash TEXT NOT NULL,
163
+ owner_instance_id TEXT NOT NULL,
164
+ owner_pid INTEGER NOT NULL,
165
+ owner_started_at TEXT NOT NULL,
166
+ entry_json TEXT NOT NULL,
167
+ agent_revision INTEGER NOT NULL DEFAULT 0,
168
+ status TEXT NOT NULL,
169
+ last_heartbeat_at INTEGER NOT NULL,
170
+ lease_expires_at INTEGER NOT NULL
171
+ );
172
+ CREATE TABLE IF NOT EXISTS resume_reservations (
173
+ reservation_id TEXT PRIMARY KEY,
174
+ target_session_id TEXT NOT NULL UNIQUE,
175
+ requester_instance_id TEXT NOT NULL,
176
+ current_session_id TEXT,
177
+ created_at INTEGER NOT NULL,
178
+ expires_at INTEGER NOT NULL
179
+ );
180
+ CREATE TABLE IF NOT EXISTS maintenance_leases (
181
+ session_id TEXT PRIMARY KEY,
182
+ operation TEXT NOT NULL,
183
+ holder_id TEXT NOT NULL,
184
+ lease_id TEXT NOT NULL UNIQUE,
185
+ acquired_at INTEGER NOT NULL,
186
+ expires_at INTEGER NOT NULL
187
+ );
188
+ CREATE TABLE IF NOT EXISTS session_agents (
189
+ session_id TEXT NOT NULL,
190
+ agent_id TEXT NOT NULL,
191
+ role TEXT,
192
+ provider TEXT,
193
+ model TEXT,
194
+ agent_session_id TEXT,
195
+ transcript_path TEXT,
196
+ parent_agent_id TEXT,
197
+ spawned_at TEXT,
198
+ ended_at TEXT,
199
+ status TEXT NOT NULL,
200
+ error TEXT,
201
+ interleaved_event_count INTEGER NOT NULL DEFAULT 0,
202
+ usage_json TEXT,
203
+ ordinal INTEGER NOT NULL DEFAULT 0,
204
+ PRIMARY KEY (session_id, agent_id)
205
+ );
206
+ CREATE TABLE IF NOT EXISTS session_agent_index (
207
+ session_id TEXT PRIMARY KEY,
208
+ transcript_size INTEGER NOT NULL,
209
+ transcript_mtime_ms REAL NOT NULL,
210
+ derived_at TEXT NOT NULL
211
+ );
212
+ CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(indexed_at DESC);
213
+ CREATE INDEX IF NOT EXISTS idx_leases_expiry ON session_leases(lease_expires_at);
214
+ CREATE INDEX IF NOT EXISTS idx_reservations_expiry ON resume_reservations(expires_at);
215
+ CREATE INDEX IF NOT EXISTS idx_maintenance_expiry ON maintenance_leases(expires_at);
216
+ `);
217
+ db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("schema_version", String(SCHEMA_VERSION));
218
+ db.prepare("INSERT INTO catalog_meta(key,value) VALUES (?,?) ON CONFLICT(key) DO NOTHING").run("generation", "0");
219
+ const row = db.prepare("SELECT value FROM catalog_meta WHERE key=?").get("schema_version");
220
+ if (Number(row.value) !== SCHEMA_VERSION)
221
+ throw new Error(`Unsupported session catalog schema ${row.value}`);
222
+ }
223
+
224
+ // src/session-catalog/store-rebuild.ts
225
+ function walkSessionFiles(root, suffix) {
226
+ const result = [];
227
+ const visit = (dir) => {
228
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
229
+ if (entry.isDirectory()) {
230
+ if (entry.name === "_cas" || entry.name === "_trash") continue;
231
+ if (shouldSkipSessionDirectoryEntry(entry.name)) continue;
232
+ visit(path.join(dir, entry.name));
233
+ } else if (entry.isFile() && entry.name.endsWith(suffix))
234
+ result.push(path.join(dir, entry.name));
235
+ }
236
+ };
237
+ visit(root);
238
+ return result;
239
+ }
240
+ function summarizeTranscriptFile(transcriptFilePath, id) {
241
+ const lines = fs.readFileSync(transcriptFilePath, "utf8").split(/\r?\n/);
242
+ let start;
243
+ let endedAt;
244
+ let lastActivityAt;
245
+ let messageCount = 0;
246
+ let iterationCount = 0;
247
+ let toolCallCount = 0;
248
+ let compactionCount = 0;
249
+ let tokenTotal = 0;
250
+ let routedModel;
251
+ let routedProvider;
252
+ for (const line of lines) {
253
+ if (!line) continue;
254
+ let event;
255
+ try {
256
+ event = parseJson(line);
257
+ } catch {
258
+ continue;
259
+ }
260
+ lastActivityAt = event.ts;
261
+ if (event.type === "session_start") start = event;
262
+ if (event.type === "session_end") endedAt = event.ts;
263
+ if (event.type === "message_appended" && (event.message.role === "user" || event.message.role === "assistant"))
264
+ messageCount++;
265
+ if (event.type === "llm_response") {
266
+ iterationCount++;
267
+ tokenTotal += totalUsageTokens(event.usage);
268
+ if (event.model) routedModel = event.model;
269
+ if (event.provider) routedProvider = event.provider;
270
+ }
271
+ if (event.type === "tool_call_end") toolCallCount++;
272
+ if (event.type === "compaction") compactionCount++;
273
+ }
274
+ if (!start) throw new Error("missing session_start");
275
+ return {
276
+ id,
277
+ title: id,
278
+ startedAt: start.ts,
279
+ ...endedAt ? { endedAt } : {},
280
+ model: routedModel ?? start.model,
281
+ provider: routedProvider ?? start.provider,
282
+ tokenTotal,
283
+ ...lastActivityAt ? { lastActivityAt } : {},
284
+ messageCount,
285
+ iterationCount,
286
+ toolCallCount,
287
+ compactionCount
288
+ };
289
+ }
290
+ function rebuildCatalogIndex(db, sessionsDir, containedPathFn, transactionFn, bumpGenerationFn) {
291
+ const summaries = walkSessionFiles(sessionsDir, ".summary.json");
292
+ const transcripts = walkSessionFiles(sessionsDir, ".jsonl").filter(
293
+ (file) => isSessionTranscriptFileName(path.basename(file))
294
+ );
295
+ const ids = /* @__PURE__ */ new Set();
296
+ for (const file of [...summaries, ...transcripts]) {
297
+ const relative2 = path.relative(sessionsDir, file).replaceAll("\\", "/");
298
+ ids.add(relative2.replace(/\.summary\.json$|\.jsonl$/, ""));
299
+ }
300
+ let indexed = 0;
301
+ let damaged = 0;
302
+ transactionFn(() => {
303
+ db.prepare("DELETE FROM sessions").run();
304
+ for (const id of ids) {
305
+ try {
306
+ const summaryFile = containedPathFn(`${id}.summary.json`);
307
+ const summary = fs.existsSync(summaryFile) ? parseJson(fs.readFileSync(summaryFile, "utf8")) : summarizeTranscriptFile(containedPathFn(`${id}.jsonl`), id);
308
+ if (!summary || summary.id !== id) throw new Error("summary identity mismatch");
309
+ const transcript = containedPathFn(`${id}.jsonl`);
310
+ const stat = fs.existsSync(transcript) ? fs.statSync(transcript) : void 0;
311
+ const now = (/* @__PURE__ */ new Date()).toISOString();
312
+ db.prepare(
313
+ "INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,transcript_size,transcript_mtime_ms,summary_revision,indexed_at,damaged) VALUES (?,?,?,?,?,?,?,?,0)"
314
+ ).run(
315
+ id,
316
+ `${id}.jsonl`,
317
+ `${id}.summary.json`,
318
+ JSON.stringify(summary),
319
+ stat?.size ?? 0,
320
+ stat?.mtimeMs ?? 0,
321
+ 1,
322
+ now
323
+ );
324
+ indexed++;
325
+ } catch {
326
+ const now = (/* @__PURE__ */ new Date()).toISOString();
327
+ const fallback = {
328
+ id,
329
+ title: id,
330
+ startedAt: now,
331
+ model: "",
332
+ provider: "",
333
+ tokenTotal: 0
334
+ };
335
+ db.prepare(
336
+ "INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,summary_revision,indexed_at,damaged) VALUES (?,?,?,?,1,?,1)"
337
+ ).run(id, `${id}.jsonl`, `${id}.summary.json`, JSON.stringify(fallback), now);
338
+ damaged++;
339
+ }
340
+ }
341
+ db.prepare(
342
+ "INSERT INTO catalog_meta(key,value) VALUES ('last_reconciliation',?) ON CONFLICT(key) DO UPDATE SET value=excluded.value"
343
+ ).run((/* @__PURE__ */ new Date()).toISOString());
344
+ bumpGenerationFn();
345
+ });
346
+ return { indexed, damaged };
347
+ }
348
+
349
+ // src/session-catalog/store.ts
350
+ var SessionCatalogStore = class {
351
+ constructor(projectDir) {
352
+ this.projectDir = projectDir;
353
+ this.sessionsDir = path2.join(projectDir, "sessions");
354
+ fs2.mkdirSync(this.sessionsDir, { recursive: true, mode: 448 });
355
+ this.databasePath = path2.join(this.sessionsDir, "catalog.sqlite");
356
+ const Database = loadDatabaseSync();
357
+ this.db = new Database(this.databasePath);
358
+ try {
359
+ this.configureDatabase();
360
+ this.initialize();
361
+ } catch (error) {
362
+ const message = error instanceof Error ? error.message : String(error);
363
+ if (!/SQLITE_CORRUPT|SQLITE_NOTADB|database disk image is malformed|file is not a database/i.test(
364
+ message
365
+ )) {
366
+ this.db.close();
367
+ throw error;
368
+ }
369
+ this.db.close();
370
+ const quarantine = `${this.databasePath}.corrupt-${Date.now()}`;
371
+ try {
372
+ fs2.renameSync(this.databasePath, quarantine);
373
+ } catch {
374
+ }
375
+ for (const suffix of ["-wal", "-shm"]) {
376
+ try {
377
+ fs2.renameSync(`${this.databasePath}${suffix}`, `${quarantine}${suffix}`);
378
+ } catch {
379
+ }
380
+ }
381
+ this.db = new Database(this.databasePath);
382
+ this.configureDatabase();
383
+ this.initialize();
384
+ }
385
+ this.reapExpired();
386
+ const rowCount = Number(
387
+ this.db.prepare("SELECT COUNT(*) AS count FROM sessions").get().count
388
+ );
389
+ if (rowCount === 0 && walkSessionFiles(this.sessionsDir, ".jsonl").some((file) => !file.endsWith("_index.jsonl"))) {
390
+ this.rebuildCatalog();
391
+ }
392
+ }
393
+ projectDir;
394
+ databasePath;
395
+ sessionsDir;
396
+ db;
397
+ scrubber = new DefaultSecretScrubber();
398
+ configureDatabase() {
399
+ configureCatalogDatabase(this.db);
400
+ }
401
+ close() {
402
+ this.db.close();
403
+ }
404
+ initialize() {
405
+ initializeCatalogSchema(this.db);
406
+ }
407
+ transaction(run) {
408
+ this.db.exec("BEGIN IMMEDIATE");
409
+ try {
410
+ const result = run();
411
+ this.db.exec("COMMIT");
412
+ return result;
413
+ } catch (error) {
414
+ this.db.exec("ROLLBACK");
415
+ throw error;
416
+ }
417
+ }
418
+ bumpGeneration() {
419
+ this.db.prepare("UPDATE catalog_meta SET value=CAST(value AS INTEGER)+1 WHERE key='generation'").run();
420
+ return this.generation();
421
+ }
422
+ generation() {
423
+ const row = this.db.prepare("SELECT value FROM catalog_meta WHERE key='generation'").get();
424
+ return Number(row.value) || 0;
425
+ }
426
+ reapExpired(now = Date.now()) {
427
+ this.db.prepare("DELETE FROM resume_reservations WHERE expires_at<=?").run(now);
428
+ this.db.prepare("DELETE FROM maintenance_leases WHERE expires_at<=?").run(now);
429
+ const rows = this.db.prepare("SELECT * FROM session_leases WHERE lease_expires_at<=?").all(now);
430
+ for (const row of rows) {
431
+ if (!isPidAlive(row.owner_pid)) {
432
+ this.db.prepare("DELETE FROM session_leases WHERE session_id=? AND lease_id=?").run(row.session_id, row.lease_id);
433
+ } else if (row.status !== "lost") {
434
+ this.db.prepare("UPDATE session_leases SET status='lost' WHERE session_id=? AND lease_id=?").run(row.session_id, row.lease_id);
435
+ }
436
+ }
437
+ }
438
+ maintenanceExists(sessionId) {
439
+ return Boolean(
440
+ this.db.prepare("SELECT 1 AS yes FROM maintenance_leases WHERE session_id=? AND expires_at>?").get(sessionId, Date.now())
441
+ );
442
+ }
443
+ leaseRow(sessionId) {
444
+ return this.db.prepare("SELECT * FROM session_leases WHERE session_id=?").get(sessionId);
445
+ }
446
+ foreignLiveLease(sessionId, callerPid = process.pid) {
447
+ const live = this.leaseRow(sessionId);
448
+ return Boolean(live) && live.owner_pid !== callerPid;
449
+ }
450
+ verifyCredential(credential) {
451
+ assertId(credential.sessionId);
452
+ const row = this.leaseRow(credential.sessionId);
453
+ if (!row || row.lease_id !== credential.leaseId || row.owner_instance_id !== credential.ownerInstanceId || !secretMatches(credential.leaseSecret, row.lease_secret_hash)) {
454
+ throw conflict(`Session ${credential.sessionId} lease proof is invalid or no longer owned`);
455
+ }
456
+ return row;
457
+ }
458
+ createLease(entry, ownerInstanceId, leaseMs) {
459
+ assertId(entry.sessionId);
460
+ if (!ownerInstanceId || ownerInstanceId.length > 256)
461
+ throw new TypeError("Invalid owner instance id");
462
+ if (!Number.isSafeInteger(entry.pid) || entry.pid <= 0)
463
+ throw new TypeError("Invalid owner pid");
464
+ const now = Date.now();
465
+ const leaseId = randomUUID();
466
+ const leaseSecret = randomBytes(32).toString("hex");
467
+ const expiresAt = now + boundedMs(leaseMs, SESSION_CATALOG_DEFAULT_LEASE_MS, MAX_LEASE_MS);
468
+ this.db.prepare(`INSERT INTO session_leases(
469
+ session_id,lease_id,lease_secret_hash,owner_instance_id,owner_pid,owner_started_at,
470
+ entry_json,agent_revision,status,last_heartbeat_at,lease_expires_at
471
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?)`).run(
472
+ entry.sessionId,
473
+ leaseId,
474
+ hashSecret(leaseSecret),
475
+ ownerInstanceId,
476
+ entry.pid,
477
+ entry.startedAt,
478
+ JSON.stringify(entry),
479
+ 0,
480
+ entry.status,
481
+ now,
482
+ expiresAt
483
+ );
484
+ return { sessionId: entry.sessionId, leaseId, leaseSecret, ownerInstanceId, expiresAt };
485
+ }
486
+ claimNew(entry, ownerInstanceId, leaseMs) {
487
+ return this.transaction(() => {
488
+ this.reapExpired();
489
+ const existing = this.leaseRow(entry.sessionId);
490
+ if (existing)
491
+ throw conflict(
492
+ `Session ${entry.sessionId} is already open in another running wstack (pid ${existing.owner_pid}).`
493
+ );
494
+ if (this.maintenanceExists(entry.sessionId))
495
+ throw conflict(`Session ${entry.sessionId} is under maintenance`);
496
+ const reserved = this.db.prepare(
497
+ "SELECT 1 AS yes FROM resume_reservations WHERE target_session_id=? AND expires_at>?"
498
+ ).get(entry.sessionId, Date.now());
499
+ if (reserved) throw conflict(`Session ${entry.sessionId} is reserved for resume`);
500
+ const credential = this.createLease(entry, ownerInstanceId, leaseMs);
501
+ this.bumpGeneration();
502
+ return credential;
503
+ });
504
+ }
505
+ reconnectLease(credential) {
506
+ return this.transaction(() => {
507
+ const row = this.verifyCredential(credential);
508
+ if (row.owner_pid !== process.pid && !isPidAlive(row.owner_pid))
509
+ throw conflict(`Session ${credential.sessionId} owner process is no longer alive`);
510
+ const expiresAt = Date.now() + SESSION_CATALOG_DEFAULT_LEASE_MS;
511
+ this.db.prepare(
512
+ "UPDATE session_leases SET lease_expires_at=?,last_heartbeat_at=?,status=? WHERE session_id=? AND lease_id=?"
513
+ ).run(
514
+ expiresAt,
515
+ Date.now(),
516
+ row.status === "lost" ? "idle" : row.status,
517
+ row.session_id,
518
+ row.lease_id
519
+ );
520
+ return { ...credential, expiresAt };
521
+ });
522
+ }
523
+ reserveResume(targetSessionId, requesterInstanceId, currentSessionId, reservationMs) {
524
+ assertId(targetSessionId);
525
+ return this.transaction(() => {
526
+ this.reapExpired();
527
+ const live = this.leaseRow(targetSessionId);
528
+ if (live)
529
+ throw conflict(
530
+ `Session ${targetSessionId} is already open in another running wstack (pid ${live.owner_pid}).`
531
+ );
532
+ if (this.maintenanceExists(targetSessionId))
533
+ throw conflict(`Session ${targetSessionId} is under maintenance`);
534
+ const catalog = this.db.prepare("SELECT 1 AS yes FROM sessions WHERE session_id=?").get(targetSessionId);
535
+ if (!catalog && !fs2.existsSync(this.containedPath(`${targetSessionId}.jsonl`)))
536
+ throw new Error(`Session not found: ${targetSessionId}`);
537
+ const reservationId = randomUUID();
538
+ const now = Date.now();
539
+ const expiresAt = now + boundedMs(reservationMs, SESSION_CATALOG_DEFAULT_RESERVATION_MS, MAX_RESERVATION_MS);
540
+ try {
541
+ this.db.prepare(
542
+ "INSERT INTO resume_reservations(reservation_id,target_session_id,requester_instance_id,current_session_id,created_at,expires_at) VALUES (?,?,?,?,?,?)"
543
+ ).run(
544
+ reservationId,
545
+ targetSessionId,
546
+ requesterInstanceId,
547
+ currentSessionId ?? null,
548
+ now,
549
+ expiresAt
550
+ );
551
+ } catch {
552
+ throw conflict(`Session ${targetSessionId} is already reserved for resume`);
553
+ }
554
+ this.bumpGeneration();
555
+ return { reservationId, targetSessionId, requesterInstanceId, expiresAt };
556
+ });
557
+ }
558
+ activateReservation(reservation, entry, leaseMs) {
559
+ return this.transaction(() => {
560
+ this.reapExpired();
561
+ const row = this.db.prepare("SELECT * FROM resume_reservations WHERE reservation_id=?").get(reservation.reservationId);
562
+ if (!row || row.target_session_id !== reservation.targetSessionId || row.requester_instance_id !== reservation.requesterInstanceId || row.expires_at <= Date.now())
563
+ throw conflict("Resume reservation expired or is not owned by this requester");
564
+ if (entry.sessionId !== row.target_session_id)
565
+ throw new TypeError("Reservation target and session entry differ");
566
+ if (this.leaseRow(entry.sessionId) || this.maintenanceExists(entry.sessionId))
567
+ throw conflict(`Session ${entry.sessionId} can no longer be activated`);
568
+ const credential = this.createLease(entry, reservation.requesterInstanceId, leaseMs);
569
+ this.db.prepare("DELETE FROM resume_reservations WHERE reservation_id=?").run(row.reservation_id);
570
+ this.bumpGeneration();
571
+ return credential;
572
+ });
573
+ }
574
+ /**
575
+ * Extend a live reservation the requester still owns.
576
+ *
577
+ * Deliberately does NOT call `reapExpired()` first: reaping would delete an
578
+ * already-expired row and turn the accurate "expired" answer into the
579
+ * indistinguishable "not owned by this requester". The caller needs to know
580
+ * which of the two happened.
581
+ */
582
+ renewReservation(reservationId, requesterInstanceId, reservationMs) {
583
+ return this.transaction(() => {
584
+ const row = this.db.prepare("SELECT * FROM resume_reservations WHERE reservation_id=?").get(reservationId);
585
+ if (!row || row.requester_instance_id !== requesterInstanceId)
586
+ throw conflict("Resume reservation is not owned by this requester");
587
+ if (row.expires_at <= Date.now()) throw conflict("Resume reservation expired");
588
+ const expiresAt = Date.now() + boundedMs(reservationMs, SESSION_CATALOG_DEFAULT_RESERVATION_MS, MAX_RESERVATION_MS);
589
+ this.db.prepare("UPDATE resume_reservations SET expires_at=? WHERE reservation_id=?").run(expiresAt, reservationId);
590
+ return {
591
+ reservationId,
592
+ targetSessionId: row.target_session_id,
593
+ requesterInstanceId,
594
+ expiresAt
595
+ };
596
+ });
597
+ }
598
+ cancelReservation(reservationId, requesterInstanceId) {
599
+ this.db.prepare("DELETE FROM resume_reservations WHERE reservation_id=? AND requester_instance_id=?").run(reservationId, requesterInstanceId);
600
+ }
601
+ heartbeat(credential, status) {
602
+ return this.transaction(() => {
603
+ const row = this.verifyCredential(credential);
604
+ const expiresAt = Date.now() + SESSION_CATALOG_DEFAULT_LEASE_MS;
605
+ const nextStatus = status ?? (row.status === "closing" ? "closing" : row.status === "lost" ? "idle" : row.status);
606
+ const entry = parseJson(row.entry_json);
607
+ entry.status = nextStatus;
608
+ entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
609
+ this.db.prepare(
610
+ "UPDATE session_leases SET status=?,entry_json=?,last_heartbeat_at=?,lease_expires_at=? WHERE session_id=? AND lease_id=?"
611
+ ).run(
612
+ nextStatus,
613
+ JSON.stringify(entry),
614
+ Date.now(),
615
+ expiresAt,
616
+ row.session_id,
617
+ row.lease_id
618
+ );
619
+ return { ...credential, expiresAt };
620
+ });
621
+ }
622
+ publishAgents(credential, revision, agents) {
623
+ if (!Number.isSafeInteger(revision) || revision < 0)
624
+ throw new TypeError("Invalid presence revision");
625
+ if (!Array.isArray(agents) || agents.length > SESSION_CATALOG_MAX_AGENTS)
626
+ throw new TypeError(`Agent snapshot exceeds ${SESSION_CATALOG_MAX_AGENTS} agents`);
627
+ const boundedAgents = boundPresenceValue(
628
+ this.scrubber.scrubObject(agents),
629
+ 0
630
+ );
631
+ const encoded = JSON.stringify(boundedAgents);
632
+ if (encoded.length > 1024 * 1024) throw new TypeError("Agent snapshot exceeds 1 MiB");
633
+ return this.transaction(() => {
634
+ const row = this.verifyCredential(credential);
635
+ if (revision <= row.agent_revision) return { accepted: false, revision: row.agent_revision };
636
+ const entry = parseJson(row.entry_json);
637
+ entry.agents = boundedAgents;
638
+ entry.agentCount = boundedAgents.length;
639
+ entry.status = boundedAgents.some((agent) => agent.status !== "idle") ? "active" : "idle";
640
+ entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
641
+ this.db.prepare(
642
+ "UPDATE session_leases SET entry_json=?,agent_revision=?,status=?,last_heartbeat_at=? WHERE session_id=? AND lease_id=?"
643
+ ).run(
644
+ JSON.stringify(entry),
645
+ revision,
646
+ entry.status,
647
+ Date.now(),
648
+ row.session_id,
649
+ row.lease_id
650
+ );
651
+ this.bumpGeneration();
652
+ return { accepted: true, revision };
653
+ });
654
+ }
655
+ markClosing(credential) {
656
+ const row = this.verifyCredential(credential);
657
+ const entry = parseJson(row.entry_json);
658
+ entry.status = "closing";
659
+ entry.lastHeartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
660
+ this.db.prepare(
661
+ "UPDATE session_leases SET status='closing',entry_json=?,last_heartbeat_at=? WHERE session_id=? AND lease_id=?"
662
+ ).run(JSON.stringify(entry), Date.now(), row.session_id, row.lease_id);
663
+ this.bumpGeneration();
664
+ }
665
+ release(credential) {
666
+ this.transaction(() => {
667
+ const row = this.verifyCredential(credential);
668
+ this.db.prepare("DELETE FROM session_leases WHERE session_id=? AND lease_id=?").run(row.session_id, row.lease_id);
669
+ this.bumpGeneration();
670
+ });
671
+ }
672
+ listLive() {
673
+ this.reapExpired();
674
+ return this.db.prepare(
675
+ "SELECT entry_json,status,last_heartbeat_at FROM session_leases ORDER BY last_heartbeat_at DESC"
676
+ ).all().map((row) => ({
677
+ ...parseJson(row.entry_json),
678
+ status: row.status,
679
+ lastHeartbeatAt: new Date(row.last_heartbeat_at).toISOString()
680
+ }));
681
+ }
682
+ getLive(sessionId) {
683
+ return this.listLive().find((entry) => entry.sessionId === sessionId) ?? null;
684
+ }
685
+ containedPath(relative2) {
686
+ assertId(relative2.replace(/\.(jsonl|summary\.json)$/, ""), "session path");
687
+ const root = path2.resolve(this.sessionsDir);
688
+ const candidate = path2.resolve(root, relative2);
689
+ const prefix = `${root}${path2.sep}`;
690
+ if (candidate !== root && !(process.platform === "win32" ? candidate.toLowerCase().startsWith(prefix.toLowerCase()) : candidate.startsWith(prefix)))
691
+ throw new TypeError("Session path escapes sessions directory");
692
+ return candidate;
693
+ }
694
+ upsertSummary(summary, transcriptRelativePath = `${summary.id}.jsonl`, summaryRelativePath = `${summary.id}.summary.json`) {
695
+ assertId(summary.id);
696
+ summary = this.scrubber.scrubObject(summary);
697
+ const normalizedTranscript = transcriptRelativePath.replaceAll("\\", "/");
698
+ const normalizedSummary = summaryRelativePath.replaceAll("\\", "/");
699
+ if (normalizedTranscript !== `${summary.id}.jsonl` || normalizedSummary !== `${summary.id}.summary.json`) {
700
+ throw new TypeError("Session catalog paths must match the canonical session identity");
701
+ }
702
+ transcriptRelativePath = normalizedTranscript;
703
+ summaryRelativePath = normalizedSummary;
704
+ const transcript = this.containedPath(transcriptRelativePath);
705
+ const stat = fs2.existsSync(transcript) ? fs2.statSync(transcript) : void 0;
706
+ const now = (/* @__PURE__ */ new Date()).toISOString();
707
+ return this.transaction(() => {
708
+ const prior = this.db.prepare("SELECT summary_revision FROM sessions WHERE session_id=?").get(summary.id);
709
+ const revision = (prior?.summary_revision ?? 0) + 1;
710
+ this.db.prepare(`INSERT INTO sessions(session_id,transcript_relative_path,summary_relative_path,summary_json,transcript_size,transcript_mtime_ms,summary_revision,indexed_at,damaged)
711
+ VALUES (?,?,?,?,?,?,?,?,0) ON CONFLICT(session_id) DO UPDATE SET transcript_relative_path=excluded.transcript_relative_path,summary_relative_path=excluded.summary_relative_path,summary_json=excluded.summary_json,transcript_size=excluded.transcript_size,transcript_mtime_ms=excluded.transcript_mtime_ms,summary_revision=excluded.summary_revision,indexed_at=excluded.indexed_at,damaged=0`).run(
712
+ summary.id,
713
+ transcriptRelativePath,
714
+ summaryRelativePath,
715
+ JSON.stringify(summary),
716
+ stat?.size ?? 0,
717
+ stat?.mtimeMs ?? 0,
718
+ revision,
719
+ now
720
+ );
721
+ this.bumpGeneration();
722
+ return {
723
+ ...summary,
724
+ transcriptRelativePath,
725
+ summaryRelativePath,
726
+ transcriptSize: stat?.size ?? 0,
727
+ transcriptMtimeMs: stat?.mtimeMs ?? 0,
728
+ summaryRevision: revision,
729
+ indexedAt: now,
730
+ damaged: false
731
+ };
732
+ });
733
+ }
734
+ catalogRecord(row) {
735
+ return {
736
+ ...parseJson(row.summary_json),
737
+ transcriptRelativePath: row.transcript_relative_path,
738
+ summaryRelativePath: row.summary_relative_path,
739
+ transcriptSize: row.transcript_size,
740
+ transcriptMtimeMs: row.transcript_mtime_ms,
741
+ summaryRevision: row.summary_revision,
742
+ indexedAt: row.indexed_at,
743
+ damaged: row.damaged !== 0
744
+ };
745
+ }
746
+ listCatalog(criteria = {}) {
747
+ const requestedLimit = criteria.limit ?? 100;
748
+ if (!Number.isFinite(requestedLimit)) throw new TypeError("Invalid session catalog limit");
749
+ const bounded = Math.min(MAX_PAGE, Math.max(1, Math.floor(requestedLimit)));
750
+ const clauses = [];
751
+ const values = [];
752
+ const jsonText = (field) => `CASE WHEN json_valid(summary_json) THEN json_extract(summary_json,'$.${field}') END`;
753
+ const literalLike = (value) => `%${value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_")}%`;
754
+ const search = criteria.search?.trim();
755
+ if (search) {
756
+ const pattern = literalLike(search);
757
+ clauses.push(
758
+ `(session_id LIKE ? ESCAPE '\\' OR ${jsonText("title")} LIKE ? ESCAPE '\\' OR ${jsonText("name")} LIKE ? ESCAPE '\\')`
759
+ );
760
+ values.push(pattern, pattern, pattern);
761
+ }
762
+ if (criteria.since) {
763
+ clauses.push(`${jsonText("startedAt")}>=?`);
764
+ values.push(criteria.since);
765
+ }
766
+ if (criteria.until) {
767
+ clauses.push(`${jsonText("startedAt")}<=?`);
768
+ values.push(criteria.until);
769
+ }
770
+ if (criteria.provider) {
771
+ clauses.push(`${jsonText("provider")}=?`);
772
+ values.push(criteria.provider);
773
+ }
774
+ if (criteria.model) {
775
+ clauses.push(`${jsonText("model")}=?`);
776
+ values.push(criteria.model);
777
+ }
778
+ if (criteria.minTokens !== void 0) {
779
+ if (!Number.isFinite(criteria.minTokens)) throw new TypeError("Invalid minimum token count");
780
+ clauses.push(`CAST(COALESCE(${jsonText("tokenTotal")},0) AS REAL)>=?`);
781
+ values.push(criteria.minTokens);
782
+ }
783
+ if (criteria.titleContains) {
784
+ clauses.push(`${jsonText("title")} LIKE ? ESCAPE '\\'`);
785
+ values.push(literalLike(criteria.titleContains.toLocaleLowerCase()));
786
+ }
787
+ const where = clauses.length > 0 ? ` WHERE ${clauses.join(" AND ")}` : "";
788
+ const rows = this.db.prepare(
789
+ `SELECT * FROM sessions${where} ORDER BY COALESCE(${jsonText("lastActivityAt")},${jsonText("endedAt")},${jsonText("startedAt")}) DESC,${jsonText("startedAt")} DESC,session_id ASC LIMIT ?`
790
+ ).all(...values, bounded);
791
+ return rows.map((row) => this.catalogRecord(row));
792
+ }
793
+ getSummary(sessionId) {
794
+ const row = this.db.prepare("SELECT * FROM sessions WHERE session_id=?").get(sessionId);
795
+ return row ? this.catalogRecord(row) : null;
796
+ }
797
+ /**
798
+ * The agent roster of one session, derived from its journal.
799
+ *
800
+ * Cached in `session_agents`, keyed to the transcript's size and mtime: the
801
+ * journal is the authority, so the cache is a memo of a pure function over
802
+ * it rather than a second place agents get recorded. When the file has grown
803
+ * (the usual case — a live session) the rows are re-derived from scratch, so
804
+ * a partial read can never leave a half-updated roster behind.
805
+ *
806
+ * Returns `[]` for a session with no transcript on disk. A caller cannot
807
+ * distinguish that from "a real session that spawned nothing", and should
808
+ * not need to: both mean there is no agent to show.
809
+ */
810
+ listSessionAgents(sessionId) {
811
+ assertId(sessionId);
812
+ const summary = this.getSummary(sessionId);
813
+ if (!summary) return [];
814
+ const file = path2.join(this.sessionsDir, summary.transcriptRelativePath);
815
+ let stat;
816
+ try {
817
+ stat = fs2.statSync(file);
818
+ } catch {
819
+ return [];
820
+ }
821
+ const cached = this.db.prepare(
822
+ "SELECT transcript_size, transcript_mtime_ms FROM session_agent_index WHERE session_id=?"
823
+ ).get(sessionId);
824
+ if (cached && Number(cached.transcript_size) === stat.size && Number(cached.transcript_mtime_ms) === stat.mtimeMs) {
825
+ return this.readSessionAgentRows(sessionId);
826
+ }
827
+ let raw;
828
+ try {
829
+ raw = fs2.readFileSync(file, "utf8");
830
+ } catch {
831
+ return [];
832
+ }
833
+ const events = [];
834
+ for (const line of raw.split("\n")) {
835
+ const trimmed = line.trim();
836
+ if (!trimmed) continue;
837
+ try {
838
+ events.push(JSON.parse(trimmed));
839
+ } catch {
840
+ }
841
+ }
842
+ const derived = deriveSessionAgents(events);
843
+ this.writeSessionAgentRows(sessionId, derived, stat.size, stat.mtimeMs);
844
+ return derived;
845
+ }
846
+ readSessionAgentRows(sessionId) {
847
+ const rows = this.db.prepare("SELECT * FROM session_agents WHERE session_id=? ORDER BY ordinal ASC").all(sessionId);
848
+ return rows.map((row) => ({
849
+ agentId: row.agent_id,
850
+ ...row.role !== null ? { role: row.role } : {},
851
+ ...row.provider !== null ? { provider: row.provider } : {},
852
+ ...row.model !== null ? { model: row.model } : {},
853
+ ...row.agent_session_id !== null ? { agentSessionId: row.agent_session_id } : {},
854
+ ...row.transcript_path !== null ? { transcriptPath: row.transcript_path } : {},
855
+ ...row.parent_agent_id !== null ? { parentAgentId: row.parent_agent_id } : {},
856
+ ...row.spawned_at !== null ? { spawnedAt: row.spawned_at } : {},
857
+ ...row.ended_at !== null ? { endedAt: row.ended_at } : {},
858
+ status: row.status,
859
+ ...row.error !== null ? { error: row.error } : {},
860
+ interleavedEventCount: Number(row.interleaved_event_count),
861
+ ...row.usage_json !== null ? { usage: parseJson(row.usage_json) } : {}
862
+ }));
863
+ }
864
+ writeSessionAgentRows(sessionId, records, size, mtimeMs) {
865
+ this.transaction(() => {
866
+ this.db.prepare("DELETE FROM session_agents WHERE session_id=?").run(sessionId);
867
+ const insert = this.db.prepare(
868
+ `INSERT INTO session_agents(session_id,agent_id,role,provider,model,agent_session_id,transcript_path,parent_agent_id,spawned_at,ended_at,status,error,interleaved_event_count,usage_json,ordinal)
869
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`
870
+ );
871
+ records.forEach((record, ordinal) => {
872
+ insert.run(
873
+ sessionId,
874
+ record.agentId,
875
+ record.role ?? null,
876
+ record.provider ?? null,
877
+ record.model ?? null,
878
+ record.agentSessionId ?? null,
879
+ record.transcriptPath ?? null,
880
+ record.parentAgentId ?? null,
881
+ record.spawnedAt ?? null,
882
+ record.endedAt ?? null,
883
+ record.status,
884
+ record.error ?? null,
885
+ record.interleavedEventCount,
886
+ record.usage ? JSON.stringify(record.usage) : null,
887
+ ordinal
888
+ );
889
+ });
890
+ this.db.prepare(
891
+ `INSERT INTO session_agent_index(session_id,transcript_size,transcript_mtime_ms,derived_at)
892
+ VALUES (?,?,?,?) ON CONFLICT(session_id) DO UPDATE SET transcript_size=excluded.transcript_size,transcript_mtime_ms=excluded.transcript_mtime_ms,derived_at=excluded.derived_at`
893
+ ).run(sessionId, size, mtimeMs, (/* @__PURE__ */ new Date()).toISOString());
894
+ });
895
+ }
896
+ resolveId(query) {
897
+ const normalized = query.trim();
898
+ if (!normalized) throw new Error("Session not found: (empty query)");
899
+ if (this.getSummary(normalized)) return normalized;
900
+ const rows = this.db.prepare(
901
+ "SELECT session_id FROM sessions WHERE session_id LIKE ? OR session_id LIKE ? LIMIT 3"
902
+ ).all(`%/${normalized}`, `${normalized}%`);
903
+ const ids = [...new Set(rows.map((row) => row.session_id))];
904
+ if (ids.length === 1) return ids[0];
905
+ if (ids.length === 0) throw new Error(`Session not found: ${query}`);
906
+ throw new Error(`Ambiguous session id "${query}": ${ids.join(", ")}`);
907
+ }
908
+ async rename(sessionId, name) {
909
+ const current = this.getSummary(this.resolveId(sessionId));
910
+ if (!current) throw new Error(`Session not found: ${sessionId}`);
911
+ const trimmed = name.trim();
912
+ const summary = { ...current };
913
+ for (const key of [
914
+ "transcriptRelativePath",
915
+ "summaryRelativePath",
916
+ "transcriptSize",
917
+ "transcriptMtimeMs",
918
+ "summaryRevision",
919
+ "indexedAt",
920
+ "damaged"
921
+ ])
922
+ delete summary[key];
923
+ const previous = { ...summary };
924
+ if (trimmed) summary.name = this.scrubber.scrub(trimmed).slice(0, 500);
925
+ else delete summary.name;
926
+ const summaryPath = this.containedPath(current.summaryRelativePath);
927
+ fs2.mkdirSync(path2.dirname(summaryPath), { recursive: true, mode: 448 });
928
+ await atomicWrite(summaryPath, `${JSON.stringify(summary)}
929
+ `, { mode: 384 });
930
+ try {
931
+ return this.upsertSummary(
932
+ summary,
933
+ current.transcriptRelativePath,
934
+ current.summaryRelativePath
935
+ );
936
+ } catch (error) {
937
+ await atomicWrite(summaryPath, `${JSON.stringify(previous)}
938
+ `, { mode: 384 }).catch(
939
+ () => void 0
940
+ );
941
+ throw error;
942
+ }
943
+ }
944
+ acquireMaintenance(sessionId, operation, holderId, leaseMs, holderPid) {
945
+ assertId(sessionId);
946
+ return this.transaction(() => {
947
+ this.reapExpired();
948
+ const live = this.leaseRow(sessionId);
949
+ if (live && (operation === "delete" || this.foreignLiveLease(sessionId, holderPid))) {
950
+ throw conflict(`Session ${sessionId} is live`);
951
+ }
952
+ const reservation = this.db.prepare(
953
+ "SELECT 1 AS yes FROM resume_reservations WHERE target_session_id=? AND expires_at>?"
954
+ ).get(sessionId, Date.now());
955
+ if (reservation) throw conflict(`Session ${sessionId} is reserved for resume`);
956
+ const leaseId = randomUUID();
957
+ const now = Date.now();
958
+ const expiresAt = now + boundedMs(leaseMs, 6e4, MAX_MAINTENANCE_MS);
959
+ try {
960
+ this.db.prepare(
961
+ "INSERT INTO maintenance_leases(session_id,operation,holder_id,lease_id,acquired_at,expires_at) VALUES (?,?,?,?,?,?)"
962
+ ).run(sessionId, operation, holderId, leaseId, now, expiresAt);
963
+ } catch {
964
+ throw conflict(`Session ${sessionId} already has maintenance in progress`);
965
+ }
966
+ return { sessionId, operation, holderId, leaseId, expiresAt };
967
+ });
968
+ }
969
+ releaseMaintenance(lease) {
970
+ this.db.prepare("DELETE FROM maintenance_leases WHERE session_id=? AND lease_id=? AND holder_id=?").run(lease.sessionId, lease.leaseId, lease.holderId);
971
+ }
972
+ delete(sessionId, lease) {
973
+ const row = this.db.prepare(
974
+ "SELECT * FROM maintenance_leases WHERE session_id=? AND lease_id=? AND holder_id=? AND operation=? AND expires_at>?"
975
+ ).get(sessionId, lease.leaseId, lease.holderId, lease.operation, Date.now());
976
+ if (!row || lease.operation !== "delete")
977
+ throw conflict("A valid delete maintenance lease is required");
978
+ const record = this.getSummary(sessionId);
979
+ if (!record) throw new Error(`Session not found: ${sessionId}`);
980
+ const transcript = this.containedPath(record.transcriptRelativePath);
981
+ const artifacts = [
982
+ transcript,
983
+ this.containedPath(record.summaryRelativePath),
984
+ this.containedPath(`${sessionId}.plan.json`),
985
+ this.containedPath(`${sessionId}.tasks.json`),
986
+ this.containedPath(`${sessionId}.todos.json`),
987
+ path2.join(path2.dirname(transcript), path2.basename(sessionId))
988
+ ];
989
+ const trashRoot = path2.join(this.sessionsDir, "_trash", lease.leaseId);
990
+ fs2.mkdirSync(trashRoot, { recursive: true, mode: 448 });
991
+ const moved = [];
992
+ try {
993
+ artifacts.forEach((artifact, index) => {
994
+ if (!fs2.existsSync(artifact)) return;
995
+ const target = path2.join(trashRoot, `${index}-${path2.basename(artifact)}`);
996
+ fs2.renameSync(artifact, target);
997
+ moved.push({ from: artifact, to: target });
998
+ });
999
+ this.transaction(() => {
1000
+ const current = this.db.prepare(
1001
+ "SELECT 1 AS yes FROM maintenance_leases WHERE session_id=? AND lease_id=? AND holder_id=? AND operation=? AND expires_at>?"
1002
+ ).get(sessionId, lease.leaseId, lease.holderId, "delete", Date.now());
1003
+ if (!current) throw conflict("Delete maintenance lease expired while staging artifacts");
1004
+ this.db.prepare("DELETE FROM sessions WHERE session_id=?").run(sessionId);
1005
+ this.db.prepare("DELETE FROM maintenance_leases WHERE session_id=?").run(sessionId);
1006
+ this.bumpGeneration();
1007
+ });
1008
+ } catch (error) {
1009
+ for (const item of moved.reverse()) {
1010
+ try {
1011
+ fs2.mkdirSync(path2.dirname(item.from), { recursive: true, mode: 448 });
1012
+ fs2.renameSync(item.to, item.from);
1013
+ } catch {
1014
+ }
1015
+ }
1016
+ throw error;
1017
+ }
1018
+ try {
1019
+ fs2.rmSync(trashRoot, { recursive: true, force: true });
1020
+ const trashParent = path2.dirname(trashRoot);
1021
+ if (fs2.readdirSync(trashParent).length === 0) fs2.rmdirSync(trashParent);
1022
+ } catch {
1023
+ }
1024
+ }
1025
+ prune(maxAgeDays, holderId) {
1026
+ if (!Number.isFinite(maxAgeDays) || maxAgeDays < 0) throw new TypeError("Invalid prune age");
1027
+ const cutoff = Date.now() - maxAgeDays * 864e5;
1028
+ const candidates = this.db.prepare("SELECT session_id FROM sessions WHERE transcript_mtime_ms<?").all(cutoff);
1029
+ let deleted = 0;
1030
+ for (const { session_id: id } of candidates) {
1031
+ try {
1032
+ const lease = this.acquireMaintenance(id, "delete", holderId);
1033
+ this.delete(id, lease);
1034
+ deleted++;
1035
+ } catch (error) {
1036
+ if (error.name !== "SessionOwnershipConflictError") throw error;
1037
+ }
1038
+ }
1039
+ return deleted;
1040
+ }
1041
+ rebuildCatalog() {
1042
+ return rebuildCatalogIndex(
1043
+ this.db,
1044
+ this.sessionsDir,
1045
+ (rel) => this.containedPath(rel),
1046
+ (run) => this.transaction(run),
1047
+ () => this.bumpGeneration()
1048
+ );
1049
+ }
1050
+ health(base) {
1051
+ this.reapExpired();
1052
+ const count = (table, where = "") => Number(
1053
+ this.db.prepare(`SELECT COUNT(*) AS count FROM ${table} ${where}`).get().count
1054
+ );
1055
+ const reconciliation = this.db.prepare("SELECT value FROM catalog_meta WHERE key='last_reconciliation'").get();
1056
+ return {
1057
+ ...base,
1058
+ catalogRows: count("sessions"),
1059
+ damagedRows: count("sessions", "WHERE damaged<>0"),
1060
+ liveLeases: count("session_leases"),
1061
+ reservations: count("resume_reservations"),
1062
+ maintenanceLeases: count("maintenance_leases"),
1063
+ generation: this.generation(),
1064
+ ...reconciliation ? { lastReconciliation: reconciliation.value } : {}
1065
+ };
1066
+ }
1067
+ };
1068
+
1069
+ export {
1070
+ deriveSessionAgents,
1071
+ SessionCatalogStore
1072
+ };
1073
+ //# sourceMappingURL=chunk-27QIURLN.js.map