@maudecode/talaria-web 1.18.0 → 1.20.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 (774) hide show
  1. package/_release.json +8 -8
  2. package/dist/api/auth-raw.js +16 -16
  3. package/dist/api/auth-raw.js.map +1 -1
  4. package/dist/api/automation-raw.d.ts +2 -2
  5. package/dist/api/automation-raw.js +93 -38
  6. package/dist/api/automation-raw.js.map +1 -1
  7. package/dist/api/automation-router.d.ts +99 -17
  8. package/dist/api/automation-router.js +22 -14
  9. package/dist/api/automation-router.js.map +1 -1
  10. package/dist/api/chat-router.d.ts +1433 -37
  11. package/dist/api/chat-router.js +205 -90
  12. package/dist/api/chat-router.js.map +1 -1
  13. package/dist/api/git-router.d.ts +6 -2
  14. package/dist/api/git-router.js +30 -30
  15. package/dist/api/git-router.js.map +1 -1
  16. package/dist/api/raw-routes.d.ts +3 -3
  17. package/dist/api/raw-routes.js +76 -62
  18. package/dist/api/raw-routes.js.map +1 -1
  19. package/dist/api/router.d.ts +5 -5
  20. package/dist/api/router.js +22 -22
  21. package/dist/api/router.js.map +1 -1
  22. package/dist/api/session-visibility.d.ts +4 -4
  23. package/dist/api/session-visibility.js +26 -16
  24. package/dist/api/session-visibility.js.map +1 -1
  25. package/dist/api/sessions-router.d.ts +2635 -151
  26. package/dist/api/sessions-router.js +193 -177
  27. package/dist/api/sessions-router.js.map +1 -1
  28. package/dist/api/settings-router.d.ts +450 -11
  29. package/dist/api/settings-router.js +102 -49
  30. package/dist/api/settings-router.js.map +1 -1
  31. package/dist/api/sse-routes.d.ts +2 -2
  32. package/dist/api/sse-routes.js +102 -80
  33. package/dist/api/sse-routes.js.map +1 -1
  34. package/dist/api/tools-raw.d.ts +5 -0
  35. package/dist/api/tools-raw.js +58 -33
  36. package/dist/api/tools-raw.js.map +1 -1
  37. package/dist/api/tools-router.d.ts +963 -109
  38. package/dist/api/tools-router.js +426 -72
  39. package/dist/api/tools-router.js.map +1 -1
  40. package/dist/api/transcribe-stream.d.ts +16 -0
  41. package/dist/api/transcribe-stream.js +172 -0
  42. package/dist/api/transcribe-stream.js.map +1 -0
  43. package/dist/app.d.ts +4 -1
  44. package/dist/app.js +80 -57
  45. package/dist/app.js.map +1 -1
  46. package/dist/auth/crypto.d.ts +3 -0
  47. package/dist/auth/crypto.js +18 -0
  48. package/dist/auth/crypto.js.map +1 -1
  49. package/dist/auth/gate.js +14 -9
  50. package/dist/auth/gate.js.map +1 -1
  51. package/dist/auth/passkeys.d.ts +15 -14
  52. package/dist/auth/passkeys.js +89 -91
  53. package/dist/auth/passkeys.js.map +1 -1
  54. package/dist/auth/store.d.ts +17 -7
  55. package/dist/auth/store.js +93 -52
  56. package/dist/auth/store.js.map +1 -1
  57. package/dist/bin/talaria-web.js +12 -81
  58. package/dist/bin/talaria-web.js.map +1 -1
  59. package/dist/cli/ctl.js +8 -2
  60. package/dist/cli/ctl.js.map +1 -1
  61. package/dist/cli/dotenv.d.ts +5 -0
  62. package/dist/cli/dotenv.js +14 -2
  63. package/dist/cli/dotenv.js.map +1 -1
  64. package/dist/cli/launcher.js +5 -5
  65. package/dist/cli/launcher.js.map +1 -1
  66. package/dist/cli/main.d.ts +1 -0
  67. package/dist/cli/main.js +90 -0
  68. package/dist/cli/main.js.map +1 -0
  69. package/dist/cli/supervise.d.ts +24 -0
  70. package/dist/cli/supervise.js +147 -6
  71. package/dist/cli/supervise.js.map +1 -1
  72. package/dist/config/agent-config.d.ts +37 -11
  73. package/dist/config/agent-config.js +146 -64
  74. package/dist/config/agent-config.js.map +1 -1
  75. package/dist/config.d.ts +2 -0
  76. package/dist/config.js +14 -5
  77. package/dist/config.js.map +1 -1
  78. package/dist/fs/atomic.d.ts +4 -0
  79. package/dist/fs/atomic.js +19 -2
  80. package/dist/fs/atomic.js.map +1 -1
  81. package/dist/fs/stat.d.ts +7 -0
  82. package/dist/fs/stat.js +27 -0
  83. package/dist/fs/stat.js.map +1 -0
  84. package/dist/http/context.d.ts +28 -9
  85. package/dist/http/context.js +77 -25
  86. package/dist/http/context.js.map +1 -1
  87. package/dist/onboarding.d.ts +2 -2
  88. package/dist/onboarding.js +10 -8
  89. package/dist/onboarding.js.map +1 -1
  90. package/dist/profiles/profiles.d.ts +12 -4
  91. package/dist/profiles/profiles.js +78 -30
  92. package/dist/profiles/profiles.js.map +1 -1
  93. package/dist/projects.d.ts +15 -6
  94. package/dist/projects.js +55 -47
  95. package/dist/projects.js.map +1 -1
  96. package/dist/providers/catalog.d.ts +85 -14
  97. package/dist/providers/catalog.js +502 -146
  98. package/dist/providers/catalog.js.map +1 -1
  99. package/dist/providers/env-file.d.ts +10 -3
  100. package/dist/providers/env-file.js +24 -75
  101. package/dist/providers/env-file.js.map +1 -1
  102. package/dist/providers/runtime-env.d.ts +4 -2
  103. package/dist/providers/runtime-env.js +9 -6
  104. package/dist/providers/runtime-env.js.map +1 -1
  105. package/dist/providers/tables.d.ts +3 -1
  106. package/dist/providers/tables.js +4 -2
  107. package/dist/providers/tables.js.map +1 -1
  108. package/dist/redact.js +56 -23
  109. package/dist/redact.js.map +1 -1
  110. package/dist/release.d.ts +7 -0
  111. package/dist/release.js +19 -3
  112. package/dist/release.js.map +1 -1
  113. package/dist/runtime.d.ts +5 -1
  114. package/dist/runtime.js +303 -96
  115. package/dist/runtime.js.map +1 -1
  116. package/dist/server.js +31 -18
  117. package/dist/server.js.map +1 -1
  118. package/dist/sessions/anchor.d.ts +11 -4
  119. package/dist/sessions/anchor.js +33 -37
  120. package/dist/sessions/anchor.js.map +1 -1
  121. package/dist/sessions/background-events.d.ts +2 -0
  122. package/dist/sessions/background-events.js +10 -0
  123. package/dist/sessions/background-events.js.map +1 -0
  124. package/dist/sessions/background-tasks.d.ts +49 -23
  125. package/dist/sessions/background-tasks.js +161 -89
  126. package/dist/sessions/background-tasks.js.map +1 -1
  127. package/dist/sessions/background-updates.d.ts +15 -8
  128. package/dist/sessions/background-updates.js +101 -25
  129. package/dist/sessions/background-updates.js.map +1 -1
  130. package/dist/sessions/cli-sessions.d.ts +23 -6
  131. package/dist/sessions/cli-sessions.js +84 -84
  132. package/dist/sessions/cli-sessions.js.map +1 -1
  133. package/dist/sessions/completions.d.ts +38 -2
  134. package/dist/sessions/completions.js +348 -77
  135. package/dist/sessions/completions.js.map +1 -1
  136. package/dist/sessions/compress.d.ts +8 -0
  137. package/dist/sessions/compress.js +2 -2
  138. package/dist/sessions/compress.js.map +1 -1
  139. package/dist/sessions/drafts.d.ts +14 -10
  140. package/dist/sessions/drafts.js +38 -43
  141. package/dist/sessions/drafts.js.map +1 -1
  142. package/dist/sessions/export.d.ts +2 -0
  143. package/dist/sessions/export.js +6 -2
  144. package/dist/sessions/export.js.map +1 -1
  145. package/dist/sessions/gateway-sessions.d.ts +86 -0
  146. package/dist/sessions/gateway-sessions.js +914 -0
  147. package/dist/sessions/gateway-sessions.js.map +1 -0
  148. package/dist/sessions/gateway-watcher.d.ts +10 -11
  149. package/dist/sessions/gateway-watcher.js +25 -21
  150. package/dist/sessions/gateway-watcher.js.map +1 -1
  151. package/dist/sessions/goals.d.ts +31 -0
  152. package/dist/sessions/goals.js +75 -0
  153. package/dist/sessions/goals.js.map +1 -0
  154. package/dist/sessions/journal.d.ts +5 -79
  155. package/dist/sessions/journal.js +15 -606
  156. package/dist/sessions/journal.js.map +1 -1
  157. package/dist/sessions/list.d.ts +34 -18
  158. package/dist/sessions/list.js +161 -147
  159. package/dist/sessions/list.js.map +1 -1
  160. package/dist/sessions/merge.d.ts +77 -13
  161. package/dist/sessions/merge.js +315 -65
  162. package/dist/sessions/merge.js.map +1 -1
  163. package/dist/sessions/notices.d.ts +22 -0
  164. package/dist/sessions/notices.js +72 -0
  165. package/dist/sessions/notices.js.map +1 -0
  166. package/dist/sessions/pending.d.ts +10 -4
  167. package/dist/sessions/pending.js +83 -7
  168. package/dist/sessions/pending.js.map +1 -1
  169. package/dist/sessions/prefix-index.d.ts +85 -0
  170. package/dist/sessions/prefix-index.js +243 -0
  171. package/dist/sessions/prefix-index.js.map +1 -0
  172. package/dist/sessions/recovery.d.ts +4 -7
  173. package/dist/sessions/recovery.js +100 -283
  174. package/dist/sessions/recovery.js.map +1 -1
  175. package/dist/sessions/relay.d.ts +20 -20
  176. package/dist/sessions/relay.js +205 -127
  177. package/dist/sessions/relay.js.map +1 -1
  178. package/dist/sessions/service.d.ts +251 -78
  179. package/dist/sessions/service.js +1510 -564
  180. package/dist/sessions/service.js.map +1 -1
  181. package/dist/sessions/session.d.ts +20 -2
  182. package/dist/sessions/session.js +33 -5
  183. package/dist/sessions/session.js.map +1 -1
  184. package/dist/sessions/shares.d.ts +7 -6
  185. package/dist/sessions/shares.js +22 -45
  186. package/dist/sessions/shares.js.map +1 -1
  187. package/dist/sessions/source-kind.js +1 -0
  188. package/dist/sessions/source-kind.js.map +1 -1
  189. package/dist/sessions/state-db-identity.d.ts +18 -0
  190. package/dist/sessions/state-db-identity.js +67 -0
  191. package/dist/sessions/state-db-identity.js.map +1 -0
  192. package/dist/sessions/state-db-worker.js +24 -5
  193. package/dist/sessions/state-db-worker.js.map +1 -1
  194. package/dist/sessions/state-db.d.ts +157 -21
  195. package/dist/sessions/state-db.js +1145 -204
  196. package/dist/sessions/state-db.js.map +1 -1
  197. package/dist/sessions/state-saved.d.ts +1 -1
  198. package/dist/sessions/state-saved.js +35 -11
  199. package/dist/sessions/state-saved.js.map +1 -1
  200. package/dist/sessions/storage/backend.d.ts +171 -0
  201. package/dist/sessions/storage/backend.js +19 -0
  202. package/dist/sessions/storage/backend.js.map +1 -0
  203. package/dist/sessions/storage/conformance.d.ts +20 -0
  204. package/dist/sessions/storage/conformance.js +237 -0
  205. package/dist/sessions/storage/conformance.js.map +1 -0
  206. package/dist/sessions/storage/document.d.ts +16 -0
  207. package/dist/sessions/storage/document.js +44 -0
  208. package/dist/sessions/storage/document.js.map +1 -0
  209. package/dist/sessions/storage/json-documents.d.ts +25 -0
  210. package/dist/sessions/storage/json-documents.js +277 -0
  211. package/dist/sessions/storage/json-documents.js.map +1 -0
  212. package/dist/sessions/storage/json-files.d.ts +23 -0
  213. package/dist/sessions/storage/json-files.js +244 -0
  214. package/dist/sessions/storage/json-files.js.map +1 -0
  215. package/dist/sessions/storage/json-installation.d.ts +3 -0
  216. package/dist/sessions/storage/json-installation.js +31 -0
  217. package/dist/sessions/storage/json-installation.js.map +1 -0
  218. package/dist/sessions/storage/json-journal.d.ts +113 -0
  219. package/dist/sessions/storage/json-journal.js +756 -0
  220. package/dist/sessions/storage/json-journal.js.map +1 -0
  221. package/dist/sessions/storage/json-recovery.d.ts +4 -0
  222. package/dist/sessions/storage/json-recovery.js +195 -0
  223. package/dist/sessions/storage/json-recovery.js.map +1 -0
  224. package/dist/sessions/storage/json.d.ts +82 -0
  225. package/dist/sessions/storage/json.js +350 -0
  226. package/dist/sessions/storage/json.js.map +1 -0
  227. package/dist/sessions/storage/row.d.ts +6 -0
  228. package/dist/sessions/storage/row.js +49 -0
  229. package/dist/sessions/storage/row.js.map +1 -0
  230. package/dist/sessions/storage/search-text.d.ts +4 -0
  231. package/dist/sessions/storage/search-text.js +16 -0
  232. package/dist/sessions/storage/search-text.js.map +1 -0
  233. package/dist/sessions/store.d.ts +67 -73
  234. package/dist/sessions/store.js +341 -607
  235. package/dist/sessions/store.js.map +1 -1
  236. package/dist/sessions/streams.d.ts +12 -7
  237. package/dist/sessions/streams.js +58 -15
  238. package/dist/sessions/streams.js.map +1 -1
  239. package/dist/sessions/titles.js +1 -1
  240. package/dist/sessions/titles.js.map +1 -1
  241. package/dist/sessions/todo.d.ts +5 -0
  242. package/dist/sessions/todo.js +28 -21
  243. package/dist/sessions/todo.js.map +1 -1
  244. package/dist/sessions/tool-display.d.ts +8 -5
  245. package/dist/sessions/tool-display.js +28 -16
  246. package/dist/sessions/tool-display.js.map +1 -1
  247. package/dist/sessions/turn-context.d.ts +10 -1
  248. package/dist/sessions/turn-context.js +13 -6
  249. package/dist/sessions/turn-context.js.map +1 -1
  250. package/dist/sessions/turn.d.ts +110 -22
  251. package/dist/sessions/turn.js +918 -380
  252. package/dist/sessions/turn.js.map +1 -1
  253. package/dist/sessions/wakeup-pause.d.ts +8 -13
  254. package/dist/sessions/wakeup-pause.js +31 -49
  255. package/dist/sessions/wakeup-pause.js.map +1 -1
  256. package/dist/sessions/window.js +13 -12
  257. package/dist/sessions/window.js.map +1 -1
  258. package/dist/settings.d.ts +19 -7
  259. package/dist/settings.js +179 -183
  260. package/dist/settings.js.map +1 -1
  261. package/dist/sidecar/client.d.ts +41 -4
  262. package/dist/sidecar/client.js +192 -42
  263. package/dist/sidecar/client.js.map +1 -1
  264. package/dist/sidecar/discover.d.ts +2 -1
  265. package/dist/sidecar/discover.js +93 -10
  266. package/dist/sidecar/discover.js.map +1 -1
  267. package/dist/sidecar/env.d.ts +4 -0
  268. package/dist/sidecar/env.js +3 -0
  269. package/dist/sidecar/env.js.map +1 -0
  270. package/dist/sidecar/fake.d.ts +6 -2
  271. package/dist/sidecar/fake.js +34 -4
  272. package/dist/sidecar/fake.js.map +1 -1
  273. package/dist/spa.d.ts +9 -4
  274. package/dist/spa.js +36 -12
  275. package/dist/spa.js.map +1 -1
  276. package/dist/startup-timing.d.ts +66 -0
  277. package/dist/startup-timing.js +129 -0
  278. package/dist/startup-timing.js.map +1 -0
  279. package/dist/test/env-file.d.ts +5 -0
  280. package/dist/test/env-file.js +88 -0
  281. package/dist/test/env-file.js.map +1 -0
  282. package/dist/test/fs-handles.d.ts +11 -0
  283. package/dist/test/fs-handles.js +31 -0
  284. package/dist/test/fs-handles.js.map +1 -0
  285. package/dist/test/harness.d.ts +1 -0
  286. package/dist/test/harness.js +1 -1
  287. package/dist/test/harness.js.map +1 -1
  288. package/dist/test/passkeys.d.ts +22 -0
  289. package/dist/test/passkeys.js +62 -0
  290. package/dist/test/passkeys.js.map +1 -0
  291. package/dist/test/storage.d.ts +15 -0
  292. package/dist/test/storage.js +26 -0
  293. package/dist/test/storage.js.map +1 -0
  294. package/dist/tools/commands.d.ts +6 -1
  295. package/dist/tools/commands.js +8 -2
  296. package/dist/tools/commands.js.map +1 -1
  297. package/dist/tools/connectors.d.ts +33 -0
  298. package/dist/tools/connectors.js +121 -0
  299. package/dist/tools/connectors.js.map +1 -0
  300. package/dist/tools/crons.d.ts +46 -5
  301. package/dist/tools/crons.js +100 -26
  302. package/dist/tools/crons.js.map +1 -1
  303. package/dist/tools/dashboard-plugins.d.ts +34 -5
  304. package/dist/tools/dashboard-plugins.js +146 -14
  305. package/dist/tools/dashboard-plugins.js.map +1 -1
  306. package/dist/tools/extensions.d.ts +5 -4
  307. package/dist/tools/extensions.js +213 -207
  308. package/dist/tools/extensions.js.map +1 -1
  309. package/dist/tools/gateway-restart.d.ts +82 -0
  310. package/dist/tools/gateway-restart.js +222 -0
  311. package/dist/tools/gateway-restart.js.map +1 -0
  312. package/dist/tools/health.d.ts +28 -4
  313. package/dist/tools/health.js +171 -50
  314. package/dist/tools/health.js.map +1 -1
  315. package/dist/tools/hygiene.d.ts +4 -3
  316. package/dist/tools/hygiene.js +23 -9
  317. package/dist/tools/hygiene.js.map +1 -1
  318. package/dist/tools/insights.d.ts +1 -1
  319. package/dist/tools/insights.js +2 -2
  320. package/dist/tools/insights.js.map +1 -1
  321. package/dist/tools/kanban.d.ts +4 -2
  322. package/dist/tools/kanban.js +28 -19
  323. package/dist/tools/kanban.js.map +1 -1
  324. package/dist/tools/mcp-health.js +74 -24
  325. package/dist/tools/mcp-health.js.map +1 -1
  326. package/dist/tools/mcp.js +19 -1
  327. package/dist/tools/mcp.js.map +1 -1
  328. package/dist/tools/memory-masks.d.ts +5 -0
  329. package/dist/tools/memory-masks.js +256 -0
  330. package/dist/tools/memory-masks.js.map +1 -0
  331. package/dist/tools/memory.d.ts +29 -3
  332. package/dist/tools/memory.js +69 -27
  333. package/dist/tools/memory.js.map +1 -1
  334. package/dist/tools/plugin-oauth.d.ts +14 -0
  335. package/dist/tools/plugin-oauth.js +53 -0
  336. package/dist/tools/plugin-oauth.js.map +1 -0
  337. package/dist/tools/plugin-settings.d.ts +15 -0
  338. package/dist/tools/plugin-settings.js +90 -0
  339. package/dist/tools/plugin-settings.js.map +1 -0
  340. package/dist/tools/prune-advice.d.ts +50 -0
  341. package/dist/tools/prune-advice.js +114 -0
  342. package/dist/tools/prune-advice.js.map +1 -0
  343. package/dist/tools/skills.d.ts +13 -5
  344. package/dist/tools/skills.js +53 -19
  345. package/dist/tools/skills.js.map +1 -1
  346. package/dist/tools/update-notifications.d.ts +102 -17
  347. package/dist/tools/update-notifications.js +400 -82
  348. package/dist/tools/update-notifications.js.map +1 -1
  349. package/dist/tools/update-rollback.d.ts +11 -0
  350. package/dist/tools/update-rollback.js +28 -0
  351. package/dist/tools/update-rollback.js.map +1 -0
  352. package/dist/tools/updates.d.ts +100 -33
  353. package/dist/tools/updates.js +1085 -345
  354. package/dist/tools/updates.js.map +1 -1
  355. package/dist/util.d.ts +2 -0
  356. package/dist/util.js +5 -0
  357. package/dist/util.js.map +1 -1
  358. package/dist/workspace/git.d.ts +16 -1
  359. package/dist/workspace/git.js +291 -11
  360. package/dist/workspace/git.js.map +1 -1
  361. package/dist/workspace/media.d.ts +12 -9
  362. package/dist/workspace/media.js +98 -38
  363. package/dist/workspace/media.js.map +1 -1
  364. package/dist/workspace/preview.js +4 -14
  365. package/dist/workspace/preview.js.map +1 -1
  366. package/dist/workspace/rollback.d.ts +8 -4
  367. package/dist/workspace/rollback.js +22 -12
  368. package/dist/workspace/rollback.js.map +1 -1
  369. package/dist/workspace/workspaces.d.ts +15 -19
  370. package/dist/workspace/workspaces.js +55 -127
  371. package/dist/workspace/workspaces.js.map +1 -1
  372. package/dist/workspace/worktrees.js +170 -32
  373. package/dist/workspace/worktrees.js.map +1 -1
  374. package/package.json +6 -4
  375. package/sidecar/agent_dependency.json +4 -4
  376. package/sidecar/agent_support.json +29 -0
  377. package/sidecar/scripts/record-fixtures.py +12 -3
  378. package/sidecar/scripts/replay_sidecar.py +8 -0
  379. package/sidecar/scripts/suite_runner.py +458 -0
  380. package/sidecar/scripts/test.sh +11 -5
  381. package/sidecar/talaria_sidecar/__init__.py +6 -1
  382. package/sidecar/talaria_sidecar/__main__.py +13 -4
  383. package/sidecar/talaria_sidecar/agent_mutation.py +1111 -0
  384. package/sidecar/talaria_sidecar/agent_services.py +1116 -0
  385. package/sidecar/talaria_sidecar/context_detail.py +59 -0
  386. package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
  387. package/sidecar/talaria_sidecar/home.py +133 -14
  388. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  389. package/sidecar/talaria_sidecar/methods/aux.py +211 -41
  390. package/sidecar/talaria_sidecar/methods/chat.py +1378 -200
  391. package/sidecar/talaria_sidecar/methods/checkpoints.py +29 -4
  392. package/sidecar/talaria_sidecar/methods/commands.py +483 -13
  393. package/sidecar/talaria_sidecar/methods/config.py +195 -6
  394. package/sidecar/talaria_sidecar/methods/connectors.py +375 -0
  395. package/sidecar/talaria_sidecar/methods/cron.py +374 -36
  396. package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
  397. package/sidecar/talaria_sidecar/methods/dashboard_plugins.py +301 -0
  398. package/sidecar/talaria_sidecar/methods/gateway.py +215 -55
  399. package/sidecar/talaria_sidecar/methods/gateway_sessions.py +418 -0
  400. package/sidecar/talaria_sidecar/methods/goals.py +81 -41
  401. package/sidecar/talaria_sidecar/methods/kanban.py +82 -30
  402. package/sidecar/talaria_sidecar/methods/memory.py +84 -0
  403. package/sidecar/talaria_sidecar/methods/oauth.py +144 -13
  404. package/sidecar/talaria_sidecar/methods/plugin_catalog.py +198 -0
  405. package/sidecar/talaria_sidecar/methods/plugin_oauth.py +234 -0
  406. package/sidecar/talaria_sidecar/methods/process.py +229 -16
  407. package/sidecar/talaria_sidecar/methods/profiles.py +48 -9
  408. package/sidecar/talaria_sidecar/methods/providers.py +196 -26
  409. package/sidecar/talaria_sidecar/methods/runtime.py +71 -1
  410. package/sidecar/talaria_sidecar/methods/skills.py +83 -5
  411. package/sidecar/talaria_sidecar/methods/state_db.py +405 -112
  412. package/sidecar/talaria_sidecar/methods/stt.py +159 -78
  413. package/sidecar/talaria_sidecar/methods/text.py +14 -1
  414. package/sidecar/talaria_sidecar/methods/toolsets.py +51 -0
  415. package/sidecar/talaria_sidecar/methods/usage.py +113 -14
  416. package/sidecar/talaria_sidecar/methods/worktree.py +145 -19
  417. package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
  418. package/sidecar/talaria_sidecar/rpc.py +70 -7
  419. package/sidecar/talaria_sidecar/runtime.py +122 -14
  420. package/sidecar/talaria_sidecar/speed.py +47 -0
  421. package/sidecar/talaria_sidecar/startup_timing.py +119 -0
  422. package/static/dist/FILES.txt +242 -215
  423. package/static/dist/THIRD_PARTY_LICENSES.md +6 -22
  424. package/static/dist/assets/AppShell-3isgvoro.js +8 -0
  425. package/static/dist/assets/Button-f4n6dapf.js +1 -0
  426. package/static/dist/assets/ChatPage-k7b6xfax.js +4 -0
  427. package/static/dist/assets/ConnectTargets-juqvt5lm.js +1 -0
  428. package/static/dist/assets/Dialog-hta5utr3.js +1 -0
  429. package/static/dist/assets/ExtensionRoute-zrhjn0wm.js +1 -0
  430. package/static/dist/assets/Field-b6ez48oz.js +1 -0
  431. package/static/dist/assets/HubRoute-iwvso2tz.js +2 -0
  432. package/static/dist/assets/LinkCheckContext-ix6zqqkk.js +1 -0
  433. package/static/dist/assets/Markdown-jdeth7zq.js +2 -0
  434. package/static/dist/assets/MarkdownRenderer-fbswsgld.js +302 -0
  435. package/static/dist/assets/MarkdownRenderer-i9o6nr38.css +1 -0
  436. package/static/dist/assets/OAuthSignIn-bpd8hom3.js +1 -0
  437. package/static/dist/assets/ResourceSwitch-odi6dy2o.js +1 -0
  438. package/static/dist/assets/RightPanel-eu6enlo6.js +1 -0
  439. package/static/dist/assets/Select-kr07928w.js +1 -0
  440. package/static/dist/assets/SessionListPanel-etb2i54l.js +1 -0
  441. package/static/dist/assets/SettingsLayout-eu3xxp23.js +1 -0
  442. package/static/dist/assets/States-icab19yg.js +1 -0
  443. package/static/dist/assets/TerminalPanel-hngmsr8b.js +16 -0
  444. package/static/dist/assets/Toaster-nb2uzjw1.js +1 -0
  445. package/static/dist/assets/_app-ga1moszl.js +1 -0
  446. package/static/dist/assets/_app.ext._extensionId-na62087t.js +1 -0
  447. package/static/dist/assets/_app.index-jypgdqoi.js +1 -0
  448. package/static/dist/assets/_app.insights-kabejdtm.js +1 -0
  449. package/static/dist/assets/_app.kanban-m19kfcmx.js +1 -0
  450. package/static/dist/assets/_app.logs-iw4m96sz.js +1 -0
  451. package/static/dist/assets/_app.memory-jq5frwcg.js +1 -0
  452. package/static/dist/assets/_app.profiles-kwa3m7rp.js +1 -0
  453. package/static/dist/assets/_app.session._sessionId-o5lyeipj.js +1 -0
  454. package/static/dist/assets/_app.settings-ckl8hdll.js +1 -0
  455. package/static/dist/assets/_app.settings._section-dtdjk98a.js +3 -0
  456. package/static/dist/assets/_app.skills-n016rihd.js +1 -0
  457. package/static/dist/assets/_app.tasks-n8zuqsqe.js +1 -0
  458. package/static/dist/assets/_app.todos-btew5v2s.js +1 -0
  459. package/static/dist/assets/_app.workspaces-lnfk95ch.js +1 -0
  460. package/static/dist/assets/abnfDiagram-VCTEODGH-dttheegb.js +1 -0
  461. package/static/dist/assets/{angular-html-nu6l31w3.js → angular-html-l28bd5fo.js} +1 -1
  462. package/static/dist/assets/{angular-ts-fbx4f8mm.js → angular-ts-ffzx1vj0.js} +1 -1
  463. package/static/dist/assets/{apl-bx2xr7dh.js → apl-b7h7mmy4.js} +1 -1
  464. package/static/dist/assets/{arc-7popfhbk.js → arc-fk1b21lm.js} +1 -1
  465. package/static/dist/assets/architecture-7GRP2DOG-ic435919.js +1 -0
  466. package/static/dist/assets/architectureDiagram-5GKGNRK7-8r3glgiq.js +36 -0
  467. package/static/dist/assets/archive-il7c6053.js +1 -0
  468. package/static/dist/assets/{astro-g77i97cb.js → astro-k117fy0t.js} +1 -1
  469. package/static/dist/assets/back-gu3nu279.js +1 -0
  470. package/static/dist/assets/{blade-boo0u92y.js → blade-npefsmn8.js} +1 -1
  471. package/static/dist/assets/{blockDiagram-I7D4REHJ-jzthp1wa.js → blockDiagram-I7D4REHJ-jenyos6c.js} +4 -4
  472. package/static/dist/assets/{c-cat0w3sc.js → c-2umg6kbn.js} +1 -1
  473. package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-nzlx7yc4.js} +1 -1
  474. package/static/dist/assets/cancel-bsnfo36u.js +1 -0
  475. package/static/dist/assets/channel-dsbtk01y.js +1 -0
  476. package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-g0cru63b.js} +20 -20
  477. package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-7rkswkjw.js} +1 -1
  478. package/static/dist/assets/chunk-DU6HZSFF-fn73jzdo.js +128 -0
  479. package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-oey8e72m.js} +1 -1
  480. package/static/dist/assets/{chunk-FOHPRMQF-opuflp73.js → chunk-FOHPRMQF-ipewusgj.js} +8 -8
  481. package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-l2a32wiy.js} +6 -6
  482. package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-o9dwzh8h.js} +1 -1
  483. package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-eblwj20b.js} +5 -5
  484. package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-o2mufspb.js} +1 -1
  485. package/static/dist/assets/chunk-OSK3NFVY-ijp9pw29.js +10 -0
  486. package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-hs9hqwl4.js} +1 -1
  487. package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-bp9rsqez.js} +1 -1
  488. package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-brkprov6.js} +1 -1
  489. package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-gmpv0pxz.js} +13 -13
  490. package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-oldm08fu.js} +4 -4
  491. package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.js → chunk-TICWLB2K-ooqilfv7.js} +6 -6
  492. package/static/dist/assets/{chunk-XXDRQBXY-n40pf9vy.js → chunk-XXDRQBXY-jkmnqu65.js} +1 -1
  493. package/static/dist/assets/classDiagram-ZZMXUADV-fvgcv24g.js +1 -0
  494. package/static/dist/assets/classDiagram-v2-VYDZK3BY-fvgcv24g.js +1 -0
  495. package/static/dist/assets/{cobol-go622aep.js → cobol-ssf9hkh7.js} +1 -1
  496. package/static/dist/assets/{coffee-jwo1xjhq.js → coffee-lsi9fwae.js} +1 -1
  497. package/static/dist/assets/copied-bz3gkazh.js +1 -0
  498. package/static/dist/assets/copy-jo006coq.js +1 -0
  499. package/static/dist/assets/cose-bilkent-JH36ORCC-layy2r27.js +1 -0
  500. package/static/dist/assets/{cpp-nre7j0kq.js → cpp-lfrh4y55.js} +1 -1
  501. package/static/dist/assets/{crystal-lviso2je.js → crystal-efh4pte7.js} +1 -1
  502. package/static/dist/assets/{css-ywz9j4hb.js → css-i82qdo89.js} +1 -1
  503. package/static/dist/assets/cynefin-OW5HDTMX-d47op7qs.js +1 -0
  504. package/static/dist/assets/cynefinDiagram-5FMLGOSQ-doa7594y.js +62 -0
  505. package/static/dist/assets/cytoscape.esm-fjn2algp.js +321 -0
  506. package/static/dist/assets/dagre-GXQ25YYZ-os768dp4.js +4 -0
  507. package/static/dist/assets/dagre-o5jmjz46.js +1 -0
  508. package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-fyi81nxd.js} +4 -4
  509. package/static/dist/assets/diagram-UQ7AKVKN-j3exggaq.js +41 -0
  510. package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-omo1en2l.js} +1 -1
  511. package/static/dist/assets/diagram-VX7I27RA-oro2cyu0.js +24 -0
  512. package/static/dist/assets/diagram-Z3DM3KII-byt9vdb7.js +24 -0
  513. package/static/dist/assets/dist-crc46di9.js +7 -0
  514. package/static/dist/assets/dist-etpoyscq.js +44 -0
  515. package/static/dist/assets/{dist-c4c2m3kv.js → dist-nb670eu6.js} +1 -1
  516. package/static/dist/assets/ebnfDiagram-PWID7BFC-ee3haugh.js +1 -0
  517. package/static/dist/assets/{edge-dvv35byi.js → edge-lrob635n.js} +1 -1
  518. package/static/dist/assets/{elixir-i2f1ki1g.js → elixir-kdpxeq93.js} +1 -1
  519. package/static/dist/assets/{elm-cpepyjm4.js → elm-c8irj9uh.js} +1 -1
  520. package/static/dist/assets/erDiagram-RLTQ6QDP-d1e93yff.js +99 -0
  521. package/static/dist/assets/{erb-idlyfheg.js → erb-flwsv146.js} +1 -1
  522. package/static/dist/assets/eventmodeling-NTZA5JFV-i1ihb40c.js +1 -0
  523. package/static/dist/assets/flowDiagram-HODETNUW-jahfpiod.js +1 -0
  524. package/static/dist/assets/folder-plus-ivoglq98.js +1 -0
  525. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ngbsyp37.js → ganttDiagram-EL5Y4UJY-m7g0jud4.js} +1 -1
  526. package/static/dist/assets/{git-rebase-d9lpfpbn.js → git-rebase-bli19aoq.js} +1 -1
  527. package/static/dist/assets/gitGraph-4MIJSDKK-hzdj07yy.js +1 -0
  528. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-dm9agbna.js} +1 -1
  529. package/static/dist/assets/{glimmer-js-39i0urfb.js → glimmer-js-d4icg38p.js} +1 -1
  530. package/static/dist/assets/{glimmer-ts-gdtd6n73.js → glimmer-ts-bk75772u.js} +1 -1
  531. package/static/dist/assets/{glsl-o1djm5pp.js → glsl-lqormp2b.js} +1 -1
  532. package/static/dist/assets/{graphql-kj6rz55t.js → graphql-hw6s5qzd.js} +1 -1
  533. package/static/dist/assets/{hack-pe1qqr2i.js → hack-ozrv6l4d.js} +1 -1
  534. package/static/dist/assets/{haml-cknnbcqs.js → haml-gytfosdt.js} +1 -1
  535. package/static/dist/assets/{handlebars-hwq64orh.js → handlebars-b6g7jffd.js} +1 -1
  536. package/static/dist/assets/highlighted-body-KPVGNVTW-mmgz2czu.js +1 -0
  537. package/static/dist/assets/host-fr6lni1p.js +1 -0
  538. package/static/dist/assets/{html-derivative-h2i2pyjc.js → html-derivative-f1tqcroo.js} +1 -1
  539. package/static/dist/assets/{html-ha05xtvt.js → html-on6oj6yj.js} +1 -1
  540. package/static/dist/assets/{http-cf94zu27.js → http-odhufsl7.js} +1 -1
  541. package/static/dist/assets/{hurl-lslax568.js → hurl-mu680336.js} +1 -1
  542. package/static/dist/assets/index-73649w7o.js +10 -0
  543. package/static/dist/assets/index-lhf1fsdf.css +2 -0
  544. package/static/dist/assets/info-A6RAGUB7-lll1dqmm.js +1 -0
  545. package/static/dist/assets/infoDiagram-27XIBGKW-k7esc639.js +2 -0
  546. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-lgr6k9wo.js} +6 -6
  547. package/static/dist/assets/{java-ndjf2o89.js → java-nxbe8uju.js} +1 -1
  548. package/static/dist/assets/{javascript-coftlwy3.js → javascript-lz3kxcyz.js} +1 -1
  549. package/static/dist/assets/{jinja-etei9wy7.js → jinja-l5pk84gt.js} +1 -1
  550. package/static/dist/assets/{jison-hyekgft1.js → jison-jdutdwyy.js} +1 -1
  551. package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-ndmbxuqk.js} +1 -1
  552. package/static/dist/assets/{json-lok0yaho.js → json-dnge4ssf.js} +1 -1
  553. package/static/dist/assets/{jsx-b64w0nbx.js → jsx-c3lwhkep.js} +1 -1
  554. package/static/dist/assets/{jsx-runtime-eu92a5pu.js → jsx-runtime-o40ukmjl.js} +1 -1
  555. package/static/dist/assets/{julia-jxxzz2yn.js → julia-hwd4rrcb.js} +1 -1
  556. package/static/dist/assets/{just-mc29iw9t.js → just-nka3oxdx.js} +1 -1
  557. package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-mjxym0jp.js} +28 -28
  558. package/static/dist/assets/katex-cbdd1ohw.css +1 -0
  559. package/static/dist/assets/katex-pbij2oo6.js +257 -0
  560. package/static/dist/assets/{latex-pafa03ft.js → latex-iqmr7e40.js} +1 -1
  561. package/static/dist/assets/lazyRouteComponent-cwb6l7xh.js +1 -0
  562. package/static/dist/assets/{line-meizqsvn.js → line-mtr5k6il.js} +1 -1
  563. package/static/dist/assets/linear-jlwrvvy6.js +1 -0
  564. package/static/dist/assets/link-iadw4ghw.js +2 -0
  565. package/static/dist/assets/{liquid-hz5c2pov.js → liquid-okmk9tbp.js} +1 -1
  566. package/static/dist/assets/locale-cs-n41hptse.js +5 -0
  567. package/static/dist/assets/locale-de-k7ht47ew.js +5 -0
  568. package/static/dist/assets/locale-es-ih078lco.js +5 -0
  569. package/static/dist/assets/locale-fr-ojabnf41.js +5 -0
  570. package/static/dist/assets/locale-it-kb96jufy.js +5 -0
  571. package/static/dist/assets/locale-ja-jpsp3xs1.js +7 -0
  572. package/static/dist/assets/locale-ko-cpzuy0n7.js +5 -0
  573. package/static/dist/assets/locale-pl-dbap4leu.js +5 -0
  574. package/static/dist/assets/locale-pt-nci7w72x.js +5 -0
  575. package/static/dist/assets/locale-ru-soghe4cs.js +7 -0
  576. package/static/dist/assets/locale-tr-ks00mu68.js +5 -0
  577. package/static/dist/assets/locale-vi-ij7twfm6.js +5 -0
  578. package/static/dist/assets/locale-zh-Hant-feakwr7a.js +5 -0
  579. package/static/dist/assets/locale-zh-ren0vjyv.js +7 -0
  580. package/static/dist/assets/login-lkhtzfcl.js +1 -0
  581. package/static/dist/assets/{lua-leyf8mlb.js → lua-krx4xj5h.js} +1 -1
  582. package/static/dist/assets/{marko-cn1fzpdv.js → marko-gbvpfehz.js} +1 -1
  583. package/static/dist/assets/{mdc-huhy88df.js → mdc-c21zlyi5.js} +1 -1
  584. package/static/dist/assets/mermaid-HWGCJPDP-jx0q5dm2.js +1 -0
  585. package/static/dist/assets/mermaid-parser.core-k91vdnvi.js +7 -0
  586. package/static/dist/assets/micromark-factory-space-fnmnpi6s.js +1 -0
  587. package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.js → mindmap-definition-YA3MSWOX-ehztfea4.js} +28 -28
  588. package/static/dist/assets/{nginx-e7lvhsmn.js → nginx-f3d68ed4.js} +1 -1
  589. package/static/dist/assets/{nim-dun5d8mk.js → nim-dw0jcy70.js} +1 -1
  590. package/static/dist/assets/onboarding-f67z0a2r.js +1 -0
  591. package/static/dist/assets/packet-AYTQ26CC-ok2xrblz.js +1 -0
  592. package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
  593. package/static/dist/assets/pegDiagram-XKGWAZYB-ec3rpj0m.js +1 -0
  594. package/static/dist/assets/{perl-bj3oqtet.js → perl-3c0r45c8.js} +1 -1
  595. package/static/dist/assets/{php-buzp12r0.js → php-rznhidvp.js} +1 -1
  596. package/static/dist/assets/pie-WAS4IAKB-iwkir23u.js +1 -0
  597. package/static/dist/assets/pieDiagram-E7YTZNPT-fcpqpekh.js +39 -0
  598. package/static/dist/assets/{pug-mjad7he9.js → pug-f2le4f3y.js} +1 -1
  599. package/static/dist/assets/{qml-d1gdp10s.js → qml-e2wbq9cr.js} +1 -1
  600. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-lqsync43.js} +1 -1
  601. package/static/dist/assets/queryKeys-ed4n2egc.js +9 -0
  602. package/static/dist/assets/{r-o3r9c7ao.js → r-epfzuw6q.js} +1 -1
  603. package/static/dist/assets/radar-RG4KPBEZ-hje4jbk6.js +1 -0
  604. package/static/dist/assets/railroad-74A4TZTK-gll9n6qi.js +1 -0
  605. package/static/dist/assets/railroad-abnf-HS5TGJTU-o6h6lpn4.js +1 -0
  606. package/static/dist/assets/railroad-ebnf-LZEXJU2U-juaefj6o.js +1 -0
  607. package/static/dist/assets/railroad-peg-WCYAUIDC-cgfhvpah.js +1 -0
  608. package/static/dist/assets/railroadDiagram-O6MQD6OU-lp3w7awf.js +1 -0
  609. package/static/dist/assets/{razor-ilc585yr.js → razor-pea1u1d0.js} +1 -1
  610. package/static/dist/assets/refresh-cw-iccm7a4e.js +1 -0
  611. package/static/dist/assets/refresh-i9ya9dvl.js +1 -0
  612. package/static/dist/assets/{regexp-mpjtcvwm.js → regexp-hzte0a5x.js} +1 -1
  613. package/static/dist/assets/registry-g7w3353u.js +1 -0
  614. package/static/dist/assets/requirementDiagram-BXWQKSXE-ha8eibcf.js +84 -0
  615. package/static/dist/assets/rolldown-runtime-bnnw9q4a.js +1 -0
  616. package/static/dist/assets/{rst-jrkwjdyi.js → rst-m6tugj5o.js} +1 -1
  617. package/static/dist/assets/{ruby-n4tjcbpz.js → ruby-l19vq3w1.js} +1 -1
  618. package/static/dist/assets/runtime-nlxpnegr.js +1 -0
  619. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-oxf31pr9.js} +1 -1
  620. package/static/dist/assets/{sas-hjo6y93k.js → sas-ktdxo397.js} +1 -1
  621. package/static/dist/assets/{scss-ncidjv48.js → scss-kn3oxfwf.js} +1 -1
  622. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.js → sequenceDiagram-WJ2MYXX4-jiq8lhyo.js} +11 -11
  623. package/static/dist/assets/settings_section_extensions_title-fadc7o65.js +1 -0
  624. package/static/dist/assets/share._token-o76ki0ly.js +2 -0
  625. package/static/dist/assets/{shellscript-m8whjpqd.js → shellscript-l100jtrg.js} +1 -1
  626. package/static/dist/assets/{shellsession-i5bj6syz.js → shellsession-m5qukwow.js} +1 -1
  627. package/static/dist/assets/sizeCapture-INFHLROL-dk1io13w.js +1 -0
  628. package/static/dist/assets/sliders-horizontal-ddcs88av.js +1 -0
  629. package/static/dist/assets/{soy-hhh36ysq.js → soy-mexlscur.js} +1 -1
  630. package/static/dist/assets/{sql-j21bzcl6.js → sql-o0ho5suh.js} +1 -1
  631. package/static/dist/assets/src-h89s0hs4.js +1 -0
  632. package/static/dist/assets/{stata-bbu2onh1.js → stata-be9ck1q3.js} +1 -1
  633. package/static/dist/assets/stateDiagram-D77RDMKH-f3fslnon.js +1 -0
  634. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hlt6yphw.js +1 -0
  635. package/static/dist/assets/{surrealql-l5vczuco.js → surrealql-gk1xkkyy.js} +1 -1
  636. package/static/dist/assets/{svelte-idn422g6.js → svelte-k6ecjilf.js} +1 -1
  637. package/static/dist/assets/swimlanes-42K2YHIH-k1hmk7jf.js +1 -0
  638. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-hd5t9tnt.js +8 -0
  639. package/static/dist/assets/{templ-e22qou3c.js → templ-gf3bftk6.js} +1 -1
  640. package/static/dist/assets/{tex-deqpc5ee.js → tex-d21fqv1z.js} +1 -1
  641. package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-n3ncfy9r.js} +8 -8
  642. package/static/dist/assets/toast-liparz2j.js +56 -0
  643. package/static/dist/assets/treeView-Q6P3EWNA-l3y2pejt.js +1 -0
  644. package/static/dist/assets/treemap-WGGIJYW6-jown9axh.js +1 -0
  645. package/static/dist/assets/{ts-tags-ctnibc27.js → ts-tags-n0g61gr6.js} +1 -1
  646. package/static/dist/assets/{tsx-i163n32g.js → tsx-cvv7wv6q.js} +1 -1
  647. package/static/dist/assets/{twig-x0fi7wm8.js → twig-n1f67204.js} +1 -1
  648. package/static/dist/assets/{typescript-g18c34xz.js → typescript-og7o55cx.js} +1 -1
  649. package/static/dist/assets/untitled-mj86r4gz.js +1 -0
  650. package/static/dist/assets/useDebounced-g8r0ggve.js +1 -0
  651. package/static/dist/assets/useForm-o4uu74mx.js +1 -0
  652. package/static/dist/assets/{useParams-b4dq6n4q.js → useParams-ftsveu84.js} +1 -1
  653. package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-crqueqga.js} +1 -1
  654. package/static/dist/assets/vennDiagram-4TSXK5OY-dh59uq1c.js +34 -0
  655. package/static/dist/assets/{vue-mwu5l1bb.js → vue-ej4xpwqw.js} +1 -1
  656. package/static/dist/assets/{vue-html-nskj3du0.js → vue-html-d48xsjuo.js} +1 -1
  657. package/static/dist/assets/{vue-vine-cqe77smq.js → vue-vine-pev88ip6.js} +1 -1
  658. package/static/dist/assets/wardley-WFR3VGLG-e6d74b0j.js +1 -0
  659. package/static/dist/assets/wardleyDiagram-VM6X3IG4-hnofh78f.js +78 -0
  660. package/static/dist/assets/with-selector-nu1b5z0v.js +1 -0
  661. package/static/dist/assets/{xml-gau7gvrv.js → xml-ifxp6gqb.js} +1 -1
  662. package/static/dist/assets/{xsl-ew09zq8b.js → xsl-fug2cckc.js} +1 -1
  663. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.js → xychartDiagram-S5SC5T6Z-bpymjelb.js} +6 -6
  664. package/static/dist/assets/{yaml-ofnrxtvf.js → yaml-fjmafnlo.js} +1 -1
  665. package/static/dist/extension-sdk.js +1 -1
  666. package/static/dist/index.html +1 -1
  667. package/static/dist/sw.js +180 -136
  668. package/static/dist/assets/AppShell-g3bkgnvt.js +0 -1
  669. package/static/dist/assets/ChatPage-oo7bdan6.js +0 -25
  670. package/static/dist/assets/Dialog-g355qxhp.js +0 -1
  671. package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +0 -1
  672. package/static/dist/assets/HubRoute-lmkrbw47.js +0 -2
  673. package/static/dist/assets/Markdown-j4kut4j2.js +0 -598
  674. package/static/dist/assets/Markdown-lwviulmg.css +0 -1
  675. package/static/dist/assets/RightPanel-ohlsdzfl.js +0 -1
  676. package/static/dist/assets/Select-kddq75qc.js +0 -1
  677. package/static/dist/assets/SessionListPanel-lu3oy0hw.js +0 -1
  678. package/static/dist/assets/SettingsLayout-o9bv4mgb.js +0 -1
  679. package/static/dist/assets/States-eq6fe29m.js +0 -1
  680. package/static/dist/assets/Toaster-pc247u9f.js +0 -1
  681. package/static/dist/assets/_app-f8n1i0m1.js +0 -1
  682. package/static/dist/assets/_app.ext._extensionId-gfbzpoou.js +0 -1
  683. package/static/dist/assets/_app.index-bzzv80hk.js +0 -1
  684. package/static/dist/assets/_app.insights-dzixbryi.js +0 -1
  685. package/static/dist/assets/_app.kanban-c6hep2us.js +0 -1
  686. package/static/dist/assets/_app.logs-i69qds98.js +0 -1
  687. package/static/dist/assets/_app.memory-kl4vf0gd.js +0 -1
  688. package/static/dist/assets/_app.profiles-b8zinmyp.js +0 -1
  689. package/static/dist/assets/_app.session._sessionId-fn1bhde6.js +0 -1
  690. package/static/dist/assets/_app.settings-cg8plwmf.js +0 -1
  691. package/static/dist/assets/_app.settings._section-bkwbi9zn.js +0 -3
  692. package/static/dist/assets/_app.skills-cs47sbvs.js +0 -1
  693. package/static/dist/assets/_app.tasks-dwvk64tc.js +0 -1
  694. package/static/dist/assets/_app.todos-c8xjm8p3.js +0 -1
  695. package/static/dist/assets/_app.workspaces-ehvd1gds.js +0 -1
  696. package/static/dist/assets/abnfDiagram-VCTEODGH-c8kdixke.js +0 -1
  697. package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
  698. package/static/dist/assets/architectureDiagram-5GKGNRK7-cku06x1p.js +0 -36
  699. package/static/dist/assets/back-h7aun7ew.js +0 -1
  700. package/static/dist/assets/cancel-fhi0vcnu.js +0 -1
  701. package/static/dist/assets/channel-b2zo73z1.js +0 -1
  702. package/static/dist/assets/chunk-DU6HZSFF-ofajdixh.js +0 -128
  703. package/static/dist/assets/chunk-OSK3NFVY-e7gkikcw.js +0 -10
  704. package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +0 -1
  705. package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +0 -1
  706. package/static/dist/assets/copied-hcplv46p.js +0 -1
  707. package/static/dist/assets/copy-magtofyp.js +0 -1
  708. package/static/dist/assets/cose-bilkent-JH36ORCC-eclcy6ja.js +0 -1
  709. package/static/dist/assets/cynefin-OW5HDTMX-siqlulkl.js +0 -1
  710. package/static/dist/assets/cynefinDiagram-5FMLGOSQ-i648tah9.js +0 -62
  711. package/static/dist/assets/cytoscape.esm-bqu53u12.js +0 -321
  712. package/static/dist/assets/dagre-GXQ25YYZ-mwn6uc17.js +0 -4
  713. package/static/dist/assets/dagre-lkoknbjn.js +0 -1
  714. package/static/dist/assets/diagram-UQ7AKVKN-mekq7zgq.js +0 -41
  715. package/static/dist/assets/diagram-VX7I27RA-xe6r5qfw.js +0 -24
  716. package/static/dist/assets/diagram-Z3DM3KII-nq07dg7k.js +0 -24
  717. package/static/dist/assets/ebnfDiagram-PWID7BFC-c62ipm3g.js +0 -1
  718. package/static/dist/assets/endpoints-klbxod4t.js +0 -64
  719. package/static/dist/assets/erDiagram-RLTQ6QDP-h39kmnzy.js +0 -99
  720. package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
  721. package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
  722. package/static/dist/assets/gitGraph-4MIJSDKK-fr2i7caa.js +0 -1
  723. package/static/dist/assets/highlighted-body-KPVGNVTW-jwpop9p8.js +0 -1
  724. package/static/dist/assets/host-i828xuo7.js +0 -1
  725. package/static/dist/assets/index-eh8zilnv.css +0 -2
  726. package/static/dist/assets/index-r648f4o1.js +0 -11
  727. package/static/dist/assets/info-A6RAGUB7-h11x0dxo.js +0 -1
  728. package/static/dist/assets/infoDiagram-27XIBGKW-dfh47dw9.js +0 -2
  729. package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
  730. package/static/dist/assets/linear-becwlnhk.js +0 -1
  731. package/static/dist/assets/link-oot2yn73.js +0 -1
  732. package/static/dist/assets/login-gu28l9ou.js +0 -1
  733. package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +0 -1
  734. package/static/dist/assets/mermaid-parser.core-ntne45i9.js +0 -7
  735. package/static/dist/assets/not-found-ccnnac39.js +0 -1
  736. package/static/dist/assets/onboarding-lkphzu1s.js +0 -1
  737. package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +0 -1
  738. package/static/dist/assets/packet-AYTQ26CC-l6jdka8p.js +0 -1
  739. package/static/dist/assets/pegDiagram-XKGWAZYB-g1er3bxj.js +0 -1
  740. package/static/dist/assets/pie-WAS4IAKB-i0ylcrt9.js +0 -1
  741. package/static/dist/assets/pieDiagram-E7YTZNPT-fbz1e55z.js +0 -39
  742. package/static/dist/assets/radar-RG4KPBEZ-f9b70aa4.js +0 -1
  743. package/static/dist/assets/railroad-74A4TZTK-lgsuoeda.js +0 -1
  744. package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +0 -1
  745. package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +0 -1
  746. package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +0 -1
  747. package/static/dist/assets/railroadDiagram-O6MQD6OU-eqpy9fnz.js +0 -1
  748. package/static/dist/assets/refresh-abtu93ke.js +0 -1
  749. package/static/dist/assets/refresh-cw-hlwb02qv.js +0 -1
  750. package/static/dist/assets/requirementDiagram-BXWQKSXE-ogf6puhi.js +0 -84
  751. package/static/dist/assets/rolldown-runtime-b9miacsj.js +0 -1
  752. package/static/dist/assets/root-mbklpuv4.js +0 -1
  753. package/static/dist/assets/saved-girnabkf.js +0 -1
  754. package/static/dist/assets/settings_section_extensions_title-h14cwk6f.js +0 -1
  755. package/static/dist/assets/share._token-kl0r7ido.js +0 -2
  756. package/static/dist/assets/sizeCapture-INFHLROL-g612l48j.js +0 -1
  757. package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
  758. package/static/dist/assets/src-hjyib4mu.js +0 -1
  759. package/static/dist/assets/sse-iwku0sjo.js +0 -1
  760. package/static/dist/assets/stateDiagram-D77RDMKH-fyoznpcs.js +0 -1
  761. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
  762. package/static/dist/assets/swimlanes-42K2YHIH-oefldhfy.js +0 -1
  763. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +0 -8
  764. package/static/dist/assets/todoStore-jjr5yr78.js +0 -1
  765. package/static/dist/assets/treeView-Q6P3EWNA-f7hcci5o.js +0 -1
  766. package/static/dist/assets/treemap-WGGIJYW6-nl85071a.js +0 -1
  767. package/static/dist/assets/units-kgxzxfhi.js +0 -1
  768. package/static/dist/assets/untitled-cikfjntn.js +0 -1
  769. package/static/dist/assets/useForm-kqoqg0k9.js +0 -1
  770. package/static/dist/assets/useLocale-o59tbiea.js +0 -1
  771. package/static/dist/assets/vennDiagram-4TSXK5OY-h5hzbk0n.js +0 -34
  772. package/static/dist/assets/wardley-WFR3VGLG-jftvpm3e.js +0 -1
  773. package/static/dist/assets/wardleyDiagram-VM6X3IG4-hedh85ge.js +0 -78
  774. /package/static/dist/assets/{ChatPage-gmu5c7lf.css → TerminalPanel-gmu5c7lf.css} +0 -0
@@ -2,18 +2,26 @@
2
2
  * Read-only projection of the Agent's `state.db` (Python `api/agent_sessions.py`):
3
3
  * importable agent session rows with compression-chain collapse, CLI visibility
4
4
  * rules, the O(1) change fingerprint used by the gateway watcher, and existence
5
- * probes. Uses `node:sqlite`; every read opens a short-lived read-only handle
6
- * with a bounded busy timeout, and no path here ever writes to the database.
5
+ * probes. Uses cached read-only `node:sqlite` handles with a bounded busy timeout
6
+ * and a fresh snapshot per read; no path here ever writes to the database.
7
7
  */
8
8
  import { existsSync, openSync, readSync, closeSync, statSync } from 'node:fs';
9
9
  import { DatabaseSync } from 'node:sqlite';
10
+ import { createHash } from 'node:crypto';
10
11
  import { Worker } from 'node:worker_threads';
12
+ import { AsyncLocalStorage } from 'node:async_hooks';
13
+ import { startupTimingScope } from '../startup-timing.js';
11
14
  import { str } from '../util.js';
15
+ import { stateDbMessageFingerprint, stateDbMessageKey } from './state-db-identity.js';
12
16
  import { MESSAGING_SOURCES } from './source-kind.js';
13
17
  import { looksLikeDefaultCliTitle, normalizeSourceName } from './titles.js';
14
18
  export const CLI_MIN_UNTITLED_USER_MESSAGE_COUNT = 2;
15
- const STATE_DB_BUSY_TIMEOUT_MS = 500;
19
+ /** The Agent's `_READ_BUSY_TIMEOUT_S` (TAL-890): a DELETE-journal commit by another process blocks readers across its fsyncs. */
20
+ const STATE_DB_BUSY_TIMEOUT_MS = 5000;
21
+ /** The gateway watcher's change check stays cheap; a lock there fails over to a full projection. */
22
+ const STATE_DB_FINGERPRINT_BUSY_TIMEOUT_MS = 500;
16
23
  const SOURCE_LABELS = {
24
+ bluebubbles: 'BlueBubbles', dingtalk: 'DingTalk', feishu: 'Feishu', qqbot: 'QQBot', yuanbao: 'Yuanbao', whatsapp_cloud: 'WhatsApp Cloud',
17
25
  acp: 'ACP', api_server: 'API', cli: 'CLI', cron: 'Cron', discord: 'Discord', email: 'Email', kanban: 'Kanban', wecom: 'WeCom', wecom_callback: 'WeCom Callback',
18
26
  slack: 'Slack', telegram: 'Telegram', tool: 'Tool', tui: 'TUI', webhook: 'Webhook', webui: 'WebUI', weixin: 'Weixin', matrix: 'Matrix', signal: 'Signal',
19
27
  };
@@ -90,6 +98,8 @@ export function isCliSessionRow(row) {
90
98
  export function isCliSessionRowVisible(row) {
91
99
  if (!isCliSessionRow(row))
92
100
  return true;
101
+ if (row._lineage_root_id && row._lineage_tip_id)
102
+ return true;
93
103
  const actual = positiveInt(row.actual_message_count);
94
104
  const messageCount = actual || positiveInt(row.message_count);
95
105
  if (messageCount <= 0)
@@ -108,9 +118,15 @@ export function isCliSessionRowVisible(row) {
108
118
  return countUserTurns(row) >= CLI_MIN_UNTITLED_USER_MESSAGE_COUNT;
109
119
  }
110
120
  // ── compression-chain projection ─────────────────────────────────────────
121
+ /** The Agent's `_CHAIN_CAP`: the most hops a walk along a compression lineage takes. Its visited check stops a cycle sooner. */
122
+ const COMPRESSION_CHAIN_CAP = 1000;
123
+ /** The most ids the Agent's `_resume_lineage_ids` returns, a tighter defensive bound than `_CHAIN_CAP`. */
124
+ const RESUME_LINEAGE_CAP = 100;
111
125
  function isContinuationSession(parent, child) {
112
126
  if (!parent || !child)
113
127
  return false;
128
+ if (lower(child.source) === 'tool' || explicitForkMarkers(child, true).length > 0)
129
+ return false;
114
130
  if (lower(child.session_source) === 'fork')
115
131
  return false;
116
132
  const parentSource = lower(parent.source);
@@ -126,6 +142,37 @@ function isContinuationSession(parent, child) {
126
142
  const ended = Number.parseFloat(str(endedAt));
127
143
  return Number.isFinite(started) && Number.isFinite(ended) && started + 1.0 >= ended;
128
144
  }
145
+ /**
146
+ * `ids` plus every session a compression continuation (`isContinuationSession`) joins to one of them, up and down, at
147
+ * most `COMPRESSION_CHAIN_CAP` in all: the rows `projectAgentSessionRows` needs to collapse their chains as the sidebar
148
+ * listing does. Forks and other children are never followed, so they cannot crowd a continuation out of the cap.
149
+ */
150
+ function compressionChainIds(db, cols, ids) {
151
+ if (!['parent_session_id', 'end_reason'].every((c) => cols.has(c)))
152
+ return ids;
153
+ const fields = ['id', 'source', 'session_source', 'model_config', 'started_at', 'parent_session_id', 'end_reason', 'ended_at'].map((c) => cols.has(c) ? c : `NULL AS ${c}`).join(', ');
154
+ const row = db.prepare(`SELECT ${fields} FROM sessions WHERE id = ?`);
155
+ const children = db.prepare(`SELECT ${fields} FROM sessions WHERE parent_session_id = ?`);
156
+ const seen = new Set(ids);
157
+ const pending = [...ids];
158
+ const visit = (next) => { if (next && !seen.has(next) && seen.size < COMPRESSION_CHAIN_CAP) {
159
+ seen.add(next);
160
+ pending.push(next);
161
+ } };
162
+ while (pending.length) {
163
+ const current = row.get(pending.pop());
164
+ if (!current)
165
+ continue;
166
+ const parentId = str(current.parent_session_id);
167
+ if (parentId && isContinuationSession(row.get(parentId), current))
168
+ visit(parentId);
169
+ if (['compression', 'cli_close'].includes(str(current.end_reason)))
170
+ for (const child of children.all(str(current.id)))
171
+ if (isContinuationSession(current, child))
172
+ visit(str(child.id));
173
+ }
174
+ return [...seen];
175
+ }
129
176
  function continuationRootId(rowsById, sessionId) {
130
177
  if (!sessionId)
131
178
  return null;
@@ -147,7 +194,7 @@ function continuationRootId(rowsById, sessionId) {
147
194
  return rootId;
148
195
  }
149
196
  /** Python `_project_agent_session_rows`: collapse compression chains into one sidebar row pointing at the freshest importable tip. */
150
- export function projectAgentSessionRows(rows) {
197
+ export function projectAgentSessionRows(rows, keepEmpty = false) {
151
198
  const rowsById = new Map(rows.map((r) => [str(r.id), r]));
152
199
  const childrenByParent = new Map();
153
200
  const continuationChildIds = new Set();
@@ -203,14 +250,22 @@ export function projectAgentSessionRows(rows) {
203
250
  };
204
251
  const projected = [];
205
252
  for (const row of rows) {
253
+ if (row._lineage_root_id && row._lineage_tip_id) {
254
+ projected.push({ ...row });
255
+ continue;
256
+ }
206
257
  if (continuationChildIds.has(str(row.id)))
207
258
  continue;
208
259
  let segments = 1;
209
260
  let tip = row;
210
261
  if (['compression', 'cli_close'].includes(str(row.end_reason)))
211
262
  [tip, segments] = compressionTip(row);
212
- if (!tip || positiveInt(tip.actual_message_count) <= 0)
263
+ // `keepEmpty` (a direct lookup of receipt-proven Web diagnostic chats, TAL-1040) keeps a turnless plain row.
264
+ if (!tip || positiveInt(tip.actual_message_count) <= 0) {
265
+ if (keepEmpty && tip === row)
266
+ projected.push({ ...row });
213
267
  continue;
268
+ }
214
269
  if (tip === row) {
215
270
  projected.push({ ...row });
216
271
  continue;
@@ -239,22 +294,266 @@ export function projectAgentSessionRows(rows) {
239
294
  projected.sort((a, b) => score(b.last_activity, b.started_at) - score(a.last_activity, a.started_at));
240
295
  return projected;
241
296
  }
242
- // ── sqlite access ─────────────────────────────────────────────────────────
243
- /** Python `open_state_db_readonly`: read-only URI open with a bounded busy timeout; the caller closes it. */
244
- export function openStateDbReadonly(dbPath, busyTimeoutMs = STATE_DB_BUSY_TIMEOUT_MS) {
245
- if (!existsSync(dbPath))
246
- throw new Error(`agent state.db not found: ${dbPath}`);
247
- const db = new DatabaseSync(dbPath, { readOnly: true, timeout: busyTimeoutMs });
248
- return db;
297
+ /** Bound descriptors and page caches even when deleted profiles are never read again. */
298
+ const STATE_DB_HANDLE_CACHE_MAX = 16;
299
+ const readonlyStateDbs = new Map();
300
+ const schemaCaches = new WeakMap();
301
+ /** Cache ownership ends at shutdown; worker termination also disposes its native SQLite resources. */
302
+ function closeStateDbs(paths = readonlyStateDbs.keys()) {
303
+ for (const path of paths) {
304
+ const cached = readonlyStateDbs.get(path);
305
+ if (cached?.db.isOpen)
306
+ cached.db.close();
307
+ readonlyStateDbs.delete(path);
308
+ }
309
+ }
310
+ process.once('exit', () => { closeStateDbs(); });
311
+ /** One handle per file identity. The caller ends the read snapshot in finally, leaving closure to the cache. */
312
+ function openStateDbReadonly(dbPath, busyTimeoutMs = STATE_DB_BUSY_TIMEOUT_MS) {
313
+ const { ino, dev, mtimeNs } = statSync(dbPath, { bigint: true });
314
+ let cached = readonlyStateDbs.get(dbPath);
315
+ // Raw writes need not advance SQLite's change counter; revalidate the main file after any such change.
316
+ if (cached && (cached.ino !== ino || cached.dev !== dev || cached.mtimeNs !== mtimeNs || !cached.db.isOpen)) {
317
+ closeStateDbs([dbPath]);
318
+ cached = undefined;
319
+ }
320
+ if (!cached) {
321
+ if (readonlyStateDbs.size >= STATE_DB_HANDLE_CACHE_MAX)
322
+ closeStateDbs([readonlyStateDbs.keys().next().value]);
323
+ const db = new DatabaseSync(dbPath, { readOnly: true, timeout: busyTimeoutMs });
324
+ cached = { db, ino, dev, mtimeNs, schemaVersion: null, columns: new Map(), indexes: new Map() };
325
+ schemaCaches.set(db, cached);
326
+ }
327
+ readonlyStateDbs.delete(dbPath);
328
+ readonlyStateDbs.set(dbPath, cached);
329
+ const { db } = cached;
330
+ try {
331
+ db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.trunc(busyTimeoutMs))}`);
332
+ // The schema cookie and all probes/queries must describe the same committed snapshot.
333
+ db.exec('BEGIN');
334
+ const version = db.prepare('PRAGMA schema_version').get().schema_version;
335
+ if (cached.schemaVersion !== version) {
336
+ cached.columns.clear();
337
+ cached.indexes.clear();
338
+ cached.schemaVersion = version;
339
+ }
340
+ return db;
341
+ }
342
+ catch (error) {
343
+ closeStateDbs([dbPath]);
344
+ throw error;
345
+ }
249
346
  }
347
+ function finishStateDbRead(db) {
348
+ try {
349
+ db.exec('COMMIT');
350
+ }
351
+ catch {
352
+ // A failed read can poison COMMIT too; discard the handle without hiding the reader's failure result.
353
+ db.close();
354
+ }
355
+ }
356
+ /** A failed probe throws, so unreadable storage is never mistaken for an older schema (TAL-785). */
250
357
  function tableColumns(db, table) {
358
+ const cache = schemaCaches.get(db).columns;
359
+ let columns = cache.get(table);
360
+ if (!columns) {
361
+ columns = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name));
362
+ cache.set(table, columns);
363
+ }
364
+ return columns;
365
+ }
366
+ function tableIndexes(db, table) {
367
+ const cache = schemaCaches.get(db).indexes;
368
+ const indexes = cache.get(table);
369
+ if (indexes)
370
+ return indexes;
251
371
  try {
252
- return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name));
372
+ const found = new Set(db.prepare(`PRAGMA index_list(${table})`).all().map((r) => r.name));
373
+ cache.set(table, found);
374
+ return found;
253
375
  }
254
376
  catch {
255
377
  return new Set();
256
378
  }
257
379
  }
380
+ /** Agent `_sql_freshest_of`: the `last_activity_at` heartbeat can lag a newer message, so it never ranks alone. */
381
+ function freshestActivity(lastActivityAt, lastMessage) {
382
+ return `MAX(COALESCE(${lastActivityAt}, 0), COALESCE(${lastMessage}, 0), COALESCE(s.started_at, 0))`;
383
+ }
384
+ /** Stale Agent statistics can favor a non-covering index and read every message row. */
385
+ function lastMessageTimestamp(indexed, alias = 'm') {
386
+ return `(SELECT MAX(${alias}.timestamp) FROM messages ${alias}${indexed ? ' INDEXED BY idx_messages_session' : ''} WHERE ${alias}.session_id = s.id)`;
387
+ }
388
+ const LOCAL_POLICY_PREFIX = 'gateway.local_policy.v1:';
389
+ /** Read-only equivalent of Agent validate_local_lineage. A receipt, never a reset edge alone, proves ownership. */
390
+ function localLineages(db, sid) {
391
+ const result = new Map();
392
+ if (!tableColumns(db, 'state_meta').has('value'))
393
+ return result;
394
+ const cols = tableColumns(db, 'sessions');
395
+ if (!['chat_id', 'user_id', 'session_key', 'model_config', 'parent_session_id', 'end_reason'].every((c) => cols.has(c)))
396
+ return result;
397
+ const meta = db.prepare('SELECT value FROM state_meta WHERE key = ?');
398
+ const fetch = db.prepare('SELECT * FROM sessions WHERE id IN (SELECT value FROM json_each(?))');
399
+ const messageCols = tableColumns(db, 'messages');
400
+ const lastMessage = ['session_id', 'timestamp'].every((c) => messageCols.has(c)) ? lastMessageTimestamp(tableIndexes(db, 'messages').has('idx_messages_session')) : 'NULL';
401
+ const activity = freshestActivity(cols.has('last_activity_at') ? 's.last_activity_at' : 'NULL', lastMessage);
402
+ // Agent _CHAIN_STEP_SQL prefers a compression child, then a live child, then recency and id.
403
+ const children = db.prepare(`SELECT s.* FROM sessions s WHERE s.parent_session_id = ? ORDER BY CASE WHEN s.end_reason = 'compression' THEN 0 WHEN s.ended_at IS NULL THEN 1 ELSE 2 END, ${activity} DESC, s.started_at DESC, s.id DESC`);
404
+ const explicitChild = (child, includeReset) => {
405
+ return lower(child.source) === 'tool' || lower(child.session_source) === 'fork'
406
+ || explicitForkMarkers(child, includeReset).length > 0;
407
+ };
408
+ const compressionChildren = (parent) => str(parent.end_reason) === 'compression'
409
+ ? children.all(str(parent.id)).filter((child) => !explicitChild(child, true)) : [];
410
+ const owner = sid ? db.prepare('SELECT chat_id FROM sessions WHERE id = ?').get(sid)?.chat_id || sid : null;
411
+ const receipts = owner
412
+ ? db.prepare('SELECT key, value FROM state_meta WHERE key = ?').all(`${LOCAL_POLICY_PREFIX}${str(owner)}`)
413
+ : db.prepare('SELECT key, value FROM state_meta WHERE key GLOB ?').iterate(`${LOCAL_POLICY_PREFIX}*`);
414
+ for (const saved of receipts) {
415
+ try {
416
+ if (saved.value.length > 1_000_000)
417
+ continue;
418
+ const receipt = JSON.parse(saved.value);
419
+ const owner = saved.key.slice(LOCAL_POLICY_PREFIX.length);
420
+ const ids = receipt.lineage;
421
+ const entry = receipt.entry;
422
+ if (receipt.session_id !== owner || !Array.isArray(ids) || ids.length < 2 || ids.length > COMPRESSION_CHAIN_CAP
423
+ || !ids.every((id) => typeof id === 'string' && Boolean(id)) || new Set(ids).size !== ids.length
424
+ || ids[0] !== owner || ids.at(-1) !== entry?.session_id)
425
+ continue;
426
+ if (!['profile_id', 'principal_id', 'request_id', 'route'].every((key) => typeof receipt[key] === 'string' && receipt[key]))
427
+ continue;
428
+ if ('legacy_session_id' in receipt) {
429
+ const legacy = meta.get(`gateway.local_legacy.v1:${owner}`);
430
+ if (receipt.legacy_session_id !== owner || !legacy)
431
+ continue;
432
+ const binding = JSON.parse(legacy.value);
433
+ if (!['session_id', 'profile_id', 'principal_id', 'legacy_session_id'].every((key) => binding[key] === receipt[key]))
434
+ continue;
435
+ }
436
+ else {
437
+ // Python json.dumps uses ensure_ascii=True, including UTF-16 surrogate pairs.
438
+ const identity = JSON.stringify([receipt.profile_id, receipt.principal_id, receipt.request_id]).replace(/[\u007f-\uffff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
439
+ if (owner !== `local-${createHash('sha256').update(identity).digest('hex')}`)
440
+ continue;
441
+ }
442
+ const byId = new Map(fetch.all(JSON.stringify(ids)).map((row) => [str(row.id), row]));
443
+ const rows = ids.map((id) => byId.get(id));
444
+ if (!rows.every((row) => Boolean(row) && row.chat_id === owner && row.user_id === receipt.principal_id && row.session_key === receipt.route))
445
+ continue;
446
+ let contextStart = 0;
447
+ let valid = true;
448
+ for (let i = 1; i < rows.length; i += 1) {
449
+ const parent = rows[i - 1], child = rows[i];
450
+ if (child.parent_session_id !== parent.id || explicitChild(child, false)) {
451
+ valid = false;
452
+ break;
453
+ }
454
+ if (parent.end_reason === 'session_reset') {
455
+ const config = jsonLoadsIfString(child.model_config);
456
+ if (!config || config._reset_from !== parent.id) {
457
+ valid = false;
458
+ break;
459
+ }
460
+ contextStart = i;
461
+ }
462
+ else {
463
+ const next = compressionChildren(parent);
464
+ if (next[0]?.id !== child.id) {
465
+ valid = false;
466
+ break;
467
+ }
468
+ }
469
+ }
470
+ if (!valid || compressionChildren(rows.at(-1)).length)
471
+ continue;
472
+ const lineage = { owner, ids, context: ids.slice(contextStart), rows, title: [...rows].reverse().find((row) => row.title)?.title ?? null };
473
+ for (const id of ids)
474
+ result.set(id, lineage);
475
+ }
476
+ catch { /* Malformed receipts cannot attach history to another conversation. */ }
477
+ }
478
+ return result;
479
+ }
480
+ /** The validated receipt's current physical target, or the original id on old Agents / unproven metadata. */
481
+ export function stateDbLocalTarget(dbPath, sid, opts = {}) {
482
+ if (!existsSync(dbPath)) {
483
+ opts.onRead?.(sid);
484
+ return sid;
485
+ }
486
+ let db;
487
+ try {
488
+ db = openStateDbReadonly(dbPath);
489
+ }
490
+ catch {
491
+ return sid;
492
+ }
493
+ try {
494
+ const target = localLineages(db, sid).get(sid)?.ids.at(-1) ?? sid;
495
+ opts.onRead?.(target);
496
+ return target;
497
+ }
498
+ catch {
499
+ return sid;
500
+ }
501
+ finally {
502
+ finishStateDbRead(db);
503
+ }
504
+ }
505
+ /** `stateDbLocalTarget` plus whether state.db answered (a missing one does), so only a real answer seeds a routing cache. */
506
+ function stateDbLocalTargetRead(dbPath, sid) {
507
+ let read = false;
508
+ const target = stateDbLocalTarget(dbPath, sid, { onRead: () => { read = true; } });
509
+ return { target, read };
510
+ }
511
+ export const AGENT_STORAGE_OK = Object.freeze({ state: 'ok', reason: null });
512
+ /**
513
+ * The corrupt-storage reason when `error` is SQLite's `SQLITE_CORRUPT` or `SQLITE_NOTADB` (the Agent's
514
+ * `hermes_state_health`), else null. The message is matched too, since a listing worker failure crosses the thread
515
+ * boundary as its message alone.
516
+ */
517
+ export function stateDbCorruptReason(error) {
518
+ const code = error?.errcode;
519
+ const message = error instanceof Error ? error.message : String(error);
520
+ const corrupt = (typeof code === 'number' && [11, 26].includes(code & 0xff)) || /malformed|not a database/i.test(message);
521
+ return corrupt ? `Agent session storage (state.db) is damaged: ${message}. Agent sessions are unavailable until it is repaired or restored from a backup.` : null;
522
+ }
523
+ /** The storage state a failed `state.db` read reports: `corrupt` when SQLite says the file is damaged, else `unavailable`. */
524
+ export function stateDbReadFailure(error) {
525
+ const corrupt = stateDbCorruptReason(error);
526
+ if (corrupt)
527
+ return { state: 'corrupt', reason: corrupt };
528
+ return { state: 'unavailable', reason: `Agent session storage (state.db) could not be read: ${error instanceof Error ? error.message : String(error)}.` };
529
+ }
530
+ function outdatedStateDbStorage() {
531
+ return { state: 'outdated', reason: "state.db has no 'source' column (older hermes-agent?). Agent sessions unavailable. Upgrade hermes-agent to fix this." };
532
+ }
533
+ /**
534
+ * TAL-785: whether `state.db` can serve the session list: `corrupt` when SQLite reports the file damaged, `unavailable`
535
+ * when it cannot be read for another reason (permissions, a lock, an I/O error), `outdated` when a readable database
536
+ * has no `sessions.source` column. A missing file is `ok`. Full-text index damage is never probed, so it stays `ok`, as
537
+ * the Agent treats it.
538
+ */
539
+ export function stateDbStorage(dbPath) {
540
+ if (!existsSync(dbPath))
541
+ return AGENT_STORAGE_OK;
542
+ try {
543
+ const db = openStateDbReadonly(dbPath);
544
+ try {
545
+ if (!tableColumns(db, 'sessions').has('source'))
546
+ return outdatedStateDbStorage();
547
+ }
548
+ finally {
549
+ finishStateDbRead(db);
550
+ }
551
+ }
552
+ catch (error) {
553
+ return stateDbReadFailure(error);
554
+ }
555
+ return AGENT_STORAGE_OK;
556
+ }
258
557
  const warnedNoSourceColumn = new Set();
259
558
  let readDelayMs = 0;
260
559
  /** Test hook: each listing read (and each listing worker request) blocks its thread this long first, standing in for a slow `state.db` (TAL-920). */
@@ -268,11 +567,15 @@ export function setStateDbReadDelayForTests(ms) { readDelayMs = ms; }
268
567
  * `idx_messages_session` exactly as Python does when its heal fails.
269
568
  */
270
569
  export function readImportableAgentSessionRows(dbPath, opts = {}) {
570
+ const result = readImportableAgentSessionRowBatches(dbPath, [opts]).rows[0];
571
+ if (result instanceof Error)
572
+ throw result;
573
+ return result;
574
+ }
575
+ /** All listing windows share one read snapshot, schema, validated local lineages and ownership lookup. */
576
+ export function readImportableAgentSessionRowBatches(dbPath, queries) {
271
577
  if (!existsSync(dbPath))
272
- return [];
273
- const limit = opts.limit === undefined ? 200 : opts.limit;
274
- const exclude = opts.excludeSources === undefined ? ['cron', 'webui'] : opts.excludeSources;
275
- const include = opts.includeSources ?? null;
578
+ return { rows: queries.map(() => []), storage: AGENT_STORAGE_OK };
276
579
  if (readDelayMs > 0)
277
580
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, readDelayMs);
278
581
  const db = openStateDbReadonly(dbPath);
@@ -282,29 +585,24 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
282
585
  if (!sessionCols.has('source')) {
283
586
  if (!warnedNoSourceColumn.has(dbPath)) {
284
587
  warnedNoSourceColumn.add(dbPath);
285
- opts.log?.(`[webui] agent session listing skipped: state.db at ${dbPath} has no 'source' column (older hermes-agent?). Agent sessions unavailable. Upgrade hermes-agent to fix this.`);
588
+ queries[0]?.log?.(`[webui] agent session listing skipped: state.db at ${dbPath} has no 'source' column (older hermes-agent?). Agent sessions unavailable. Upgrade hermes-agent to fix this.`);
286
589
  }
287
- return [];
590
+ return { rows: queries.map(() => []), storage: outdatedStateDbStorage() };
288
591
  }
592
+ const only = queries.length === 1 ? queries[0]?.sessionIds : undefined;
593
+ const local = localLineages(db, only?.length === 1 ? only[0] : undefined);
289
594
  const col = (name) => (sessionCols.has(name) ? `s.${name}` : `NULL AS ${name}`);
290
595
  const messagesHasSessionId = messageCols.has('session_id');
291
596
  const messagesHasTimestamp = messageCols.has('timestamp');
292
597
  const countCol = messageCols.has('id') ? 'id' : 'session_id';
293
- let messagesIndexPresent = false;
294
- let userMessagesIndexPresent = false;
295
- if (messagesHasSessionId && messagesHasTimestamp) {
296
- try {
297
- const indexes = new Set(db.prepare('PRAGMA index_list(messages)').all().map((r) => r.name));
298
- messagesIndexPresent = indexes.has('idx_messages_session');
299
- userMessagesIndexPresent = indexes.has('idx_messages_session_user');
300
- }
301
- catch {
302
- messagesIndexPresent = false;
303
- }
304
- }
598
+ const messageIndexes = messagesHasSessionId && messagesHasTimestamp ? tableIndexes(db, 'messages') : new Set();
599
+ const sessionIndexes = tableIndexes(db, 'sessions');
600
+ const hasPolicies = Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'state_meta'").get());
601
+ const messagesIndexPresent = messageIndexes.has('idx_messages_session');
602
+ const userMessagesIndexPresent = messageIndexes.has('idx_messages_session_user');
305
603
  let actualCountExpr;
306
604
  let userCountExpr;
307
- let lastActivityExpr;
605
+ let baseLastActivityExpr;
308
606
  let joinClause = '';
309
607
  let groupByClause = '';
310
608
  if (messagesHasSessionId) {
@@ -317,12 +615,12 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
317
615
  }
318
616
  else
319
617
  userCountExpr = actualCountExpr;
320
- lastActivityExpr = messagesHasTimestamp ? '(SELECT MAX(lm.timestamp) FROM messages lm WHERE lm.session_id = s.id)' : 'NULL';
618
+ baseLastActivityExpr = messagesHasTimestamp ? lastMessageTimestamp(messagesIndexPresent, 'lm') : 'NULL';
321
619
  }
322
620
  else {
323
621
  actualCountExpr = `COUNT(m.${countCol})`;
324
622
  userCountExpr = messageCols.has('role') ? "EXISTS(SELECT 1 FROM messages um WHERE um.session_id = s.id AND LOWER(um.role) = 'user')" : `COUNT(m.${countCol})`;
325
- lastActivityExpr = messagesHasTimestamp ? 'MAX(m.timestamp)' : 'NULL';
623
+ baseLastActivityExpr = messagesHasTimestamp ? 'MAX(m.timestamp)' : 'NULL';
326
624
  joinClause = 'LEFT JOIN messages m ON m.session_id = s.id';
327
625
  groupByClause = 'GROUP BY s.id';
328
626
  }
@@ -330,75 +628,151 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
330
628
  else {
331
629
  actualCountExpr = 's.message_count';
332
630
  userCountExpr = 's.message_count';
333
- lastActivityExpr = 'NULL';
334
- }
335
- let orderBy = 'ORDER BY s.started_at DESC';
336
- let latestMessagesCte = null;
337
- let candidateOrder = 'ORDER BY s.started_at DESC';
338
- const where = ['s.source IS NOT NULL'];
339
- const params = [];
340
- const included = (include ?? []).map(String).filter(Boolean);
341
- if (included.length) {
342
- where.push(`s.source IN (${included.map(() => '?').join(', ')})`);
343
- params.push(...included);
631
+ baseLastActivityExpr = 'NULL';
344
632
  }
345
- const excluded = (exclude ?? []).map(String).filter(Boolean);
346
- if (excluded.length) {
347
- where.push(`s.source NOT IN (${excluded.map(() => '?').join(', ')})`);
348
- params.push(...excluded);
349
- }
350
- const preaggregated = messagesHasSessionId && messagesHasTimestamp && included.length === 1 && included[0] === 'cron' && !messagesIndexPresent;
351
- if (preaggregated) {
352
- orderBy = 'ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC';
353
- latestMessagesCte = 'latest_messages AS (SELECT mx.session_id AS session_id, MAX(mx.timestamp) AS last_message_at FROM messages mx GROUP BY mx.session_id)';
354
- candidateOrder = 'ORDER BY COALESCE(lm.last_message_at, s.started_at) DESC, s.started_at DESC';
355
- }
356
- else if (messagesHasSessionId && messagesHasTimestamp) {
357
- orderBy = messagesIndexPresent ? 'ORDER BY COALESCE(last_activity, s.started_at) DESC' : 'ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC';
358
- candidateOrder = 'ORDER BY COALESCE((SELECT MAX(mx.timestamp) FROM messages mx WHERE mx.session_id = s.id), s.started_at) DESC, s.started_at DESC';
359
- }
360
- const selectSql = `SELECT s.id, s.title, s.model, s.message_count, s.started_at, s.source, ${['session_source', 'user_id', 'chat_id', 'chat_type', 'thread_id', 'session_key', 'origin_chat_id', 'origin_user_id', 'platform', 'parent_session_id', 'ended_at', 'end_reason'].map(col).join(', ')}, ${actualCountExpr} AS actual_message_count, ${userCountExpr} AS actual_user_message_count, ${lastActivityExpr} AS last_activity`;
361
- let raw;
362
- if (limit !== null) {
363
- const resultLimit = Math.max(0, Math.trunc(limit));
364
- if (resultLimit === 0)
365
- return [];
366
- const candidateLimit = Math.max(resultLimit * 8, resultLimit);
367
- const candidateCte = latestMessagesCte
368
- ? `WITH ${latestMessagesCte}, candidates AS (SELECT s.id FROM sessions s LEFT JOIN latest_messages lm ON lm.session_id = s.id WHERE ${where.join(' AND ')} ${candidateOrder} LIMIT ?)`
369
- : `WITH candidates AS (SELECT s.id FROM sessions s WHERE ${where.join(' AND ')} ${candidateOrder} LIMIT ?)`;
370
- raw = db.prepare(`${candidateCte} ${selectSql} FROM sessions s JOIN candidates c ON c.id = s.id ${joinClause} ${groupByClause} ${orderBy}`).all(...params, candidateLimit);
371
- }
372
- else {
373
- raw = db.prepare(`${selectSql} FROM sessions s ${joinClause} WHERE ${where.join(' AND ')} ${groupByClause} ${orderBy}`).all(...params);
633
+ const candidates = (opts) => {
634
+ const limit = opts.limit === undefined ? 200 : opts.limit;
635
+ const exclude = opts.excludeSources === undefined ? ['cron', 'webui'] : opts.excludeSources;
636
+ const include = opts.includeSources ?? null;
637
+ let lastActivityExpr = baseLastActivityExpr;
638
+ let orderBy = 'ORDER BY s.started_at DESC';
639
+ let latestMessagesCte = null;
640
+ let candidateOrder = 'ORDER BY s.started_at DESC';
641
+ const where = ['s.source IS NOT NULL'];
642
+ const params = [];
643
+ if (opts.sessionIds !== undefined) {
644
+ where.push('s.id IN (SELECT value FROM json_each(?))');
645
+ const ids = opts.sessionIds.map((id) => local.get(id)?.ids.at(-1) ?? id);
646
+ params.push(JSON.stringify(opts.compressionChains ? compressionChainIds(db, sessionCols, ids) : ids));
647
+ }
648
+ const superseded = [...local].filter(([id, lineage]) => id !== lineage.ids.at(-1)).map(([id]) => id);
649
+ if (superseded.length) {
650
+ where.push('s.id NOT IN (SELECT value FROM json_each(?))');
651
+ params.push(JSON.stringify(superseded));
652
+ }
653
+ const policySource = (id) => `(SELECT CASE WHEN json_valid(value) THEN json_extract(value, '$.policy.source') END FROM state_meta WHERE key = '${LOCAL_POLICY_PREFIX}' || ${id})`;
654
+ const sourceSql = hasPolicies ? `COALESCE(${policySource('s.id')}, ${sessionCols.has('chat_id') ? policySource('s.chat_id') : 'NULL'}, s.source)` : 's.source';
655
+ const included = (include ?? []).map(String).filter(Boolean);
656
+ if (included.length) {
657
+ where.push(`${sourceSql} IN (${included.map(() => '?').join(', ')})`);
658
+ params.push(...included);
659
+ }
660
+ const excluded = (exclude ?? []).map(String).filter(Boolean);
661
+ if (excluded.length) {
662
+ where.push(`${sourceSql} NOT IN (${excluded.map(() => '?').join(', ')})`);
663
+ params.push(...excluded);
664
+ }
665
+ const preaggregated = messagesHasSessionId && messagesHasTimestamp && included.length === 1 && included[0] === 'cron' && !messagesIndexPresent;
666
+ // TAL-1163: the Agent's activity index preselects the window; exact recency is computed for those candidates only.
667
+ const activityIndexed = limit !== null && messagesHasSessionId && messagesHasTimestamp && sessionCols.has('last_activity_at')
668
+ && sessionIndexes.has('idx_sessions_effective_activity');
669
+ if (activityIndexed) {
670
+ lastActivityExpr = freshestActivity('s.last_activity_at', lastActivityExpr);
671
+ orderBy = 'ORDER BY last_activity DESC';
672
+ candidateOrder = 'ORDER BY COALESCE(s.last_activity_at, s.started_at) DESC, s.started_at DESC, s.id DESC';
673
+ }
674
+ else if (preaggregated) {
675
+ orderBy = 'ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC';
676
+ latestMessagesCte = 'latest_messages AS (SELECT mx.session_id AS session_id, MAX(mx.timestamp) AS last_message_at FROM messages mx GROUP BY mx.session_id)';
677
+ candidateOrder = 'ORDER BY COALESCE(lm.last_message_at, s.started_at) DESC, s.started_at DESC';
678
+ }
679
+ else if (messagesHasSessionId && messagesHasTimestamp) {
680
+ orderBy = messagesIndexPresent ? 'ORDER BY COALESCE(last_activity, s.started_at) DESC' : 'ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC';
681
+ candidateOrder = `ORDER BY COALESCE(${lastMessageTimestamp(messagesIndexPresent, 'mx')}, s.started_at) DESC, s.started_at DESC`;
682
+ }
683
+ const selectSql = `SELECT s.id, s.title, s.model, s.message_count, s.started_at, s.source, ${['model_config', 'session_source', 'user_id', 'chat_id', 'chat_type', 'thread_id', 'session_key', 'origin_chat_id', 'origin_user_id', 'platform', 'parent_session_id', 'ended_at', 'end_reason'].map(col).join(', ')}, ${actualCountExpr} AS actual_message_count, ${userCountExpr} AS actual_user_message_count, ${lastActivityExpr} AS last_activity`;
684
+ let raw;
685
+ if (limit !== null) {
686
+ const resultLimit = Math.max(0, Math.trunc(limit));
687
+ if (resultLimit === 0)
688
+ return [];
689
+ const candidateLimit = Math.max(resultLimit * 8, resultLimit);
690
+ const candidateCte = latestMessagesCte
691
+ ? `WITH ${latestMessagesCte}, candidates AS (SELECT s.id FROM sessions s LEFT JOIN latest_messages lm ON lm.session_id = s.id WHERE ${where.join(' AND ')} ${candidateOrder} LIMIT ?)`
692
+ : `WITH candidates AS (SELECT s.id FROM sessions s WHERE ${where.join(' AND ')} ${candidateOrder} LIMIT ?)`;
693
+ raw = db.prepare(`${candidateCte} ${selectSql} FROM sessions s JOIN candidates c ON c.id = s.id ${joinClause} ${groupByClause} ${orderBy}`).all(...params, candidateLimit);
694
+ }
695
+ else {
696
+ raw = db.prepare(`${selectSql} FROM sessions s ${joinClause} WHERE ${where.join(' AND ')} ${groupByClause} ${orderBy}`).all(...params);
697
+ }
698
+ return raw;
699
+ };
700
+ const batches = queries.map((opts) => {
701
+ try {
702
+ return candidates(opts);
703
+ }
704
+ catch (error) {
705
+ return error;
706
+ }
707
+ });
708
+ const ids = [...new Set(batches.flatMap((rows) => rows instanceof Error ? [] : rows.map((r) => str(r.id))))];
709
+ let owners;
710
+ try {
711
+ owners = sessionSources(db, sessionCols, ids);
374
712
  }
375
- let projected = projectAgentSessionRows(raw.map((r) => ({ ...r })));
376
- projected = projected.map((r) => ({ ...r, ...normalizeAgentSessionSource(r.source) }));
377
- projected = projected.filter(isCliSessionRowVisible);
378
- if (limit === null)
379
- return projected;
380
- const selected = projected.slice(0, Math.max(0, Math.trunc(limit)));
381
- const have = new Set(selected.map((r) => str(r.id)));
382
- const byId = new Map(projected.filter((r) => r.id).map((r) => [str(r.id), r]));
383
- const pending = [...selected];
384
- while (pending.length) {
385
- const row = pending.pop();
386
- if (lower(row.raw_source || row.source) !== 'subagent')
387
- continue;
388
- const parentId = str(row.parent_session_id);
389
- if (!parentId || have.has(parentId))
390
- continue;
391
- const parent = byId.get(parentId);
392
- if (!parent || lower(parent.raw_source || parent.source) !== 'subagent')
393
- continue;
394
- selected.push(parent);
395
- have.add(parentId);
396
- pending.push(parent);
713
+ catch (error) {
714
+ if (queries.length === 1)
715
+ throw error;
716
+ // Retry ownership per window only on damage, so a background failure cannot hide healthy interactive rows.
397
717
  }
398
- return selected;
718
+ const rows = batches.map((batch, i) => {
719
+ if (batch instanceof Error)
720
+ return batch;
721
+ let batchOwners = owners;
722
+ if (batchOwners === undefined) {
723
+ try {
724
+ batchOwners = sessionSources(db, sessionCols, batch.map((row) => str(row.id)));
725
+ }
726
+ catch (error) {
727
+ return error;
728
+ }
729
+ }
730
+ let raw = batch;
731
+ const opts = queries[i];
732
+ const limit = opts.limit === undefined ? 200 : opts.limit;
733
+ raw = raw.map((r) => { const owner = batchOwners.get(str(r.id)); return { ...r, source: typeof owner === 'object' ? owner.source : owner || r.source }; });
734
+ const keepEmpty = opts.keepEmpty === true && opts.sessionIds !== undefined;
735
+ let projected = projectAgentSessionRows(raw.map((r) => {
736
+ const lineage = local.get(str(r.id));
737
+ if (!lineage)
738
+ return { ...r };
739
+ const root = lineage.rows[0];
740
+ const title = lineage.title || r.title;
741
+ return { ...r, title, started_at: root.started_at, last_activity: r.last_activity ?? r.started_at, parent_session_id: root.parent_session_id,
742
+ _lineage_root_id: lineage.owner, _lineage_tip_id: r.id, _lineage_ids: [...lineage.ids], _compression_segment_count: lineage.ids.length };
743
+ }), keepEmpty);
744
+ projected = projected.map((r) => {
745
+ const parent = local.get(str(r.parent_session_id));
746
+ return { ...r, ...normalizeAgentSessionSource(r.source), ...(parent ? { _parent_lineage_root_id: parent.owner, parent_title: parent.title, parent_source: parent.rows.at(-1)?.source ?? null } : {}) };
747
+ });
748
+ if (!keepEmpty)
749
+ projected = projected.filter(isCliSessionRowVisible);
750
+ if (limit === null)
751
+ return projected;
752
+ const selected = projected.slice(0, Math.max(0, Math.trunc(limit)));
753
+ const have = new Set(selected.map((r) => str(r.id)));
754
+ const byId = new Map(projected.filter((r) => r.id).map((r) => [str(r.id), r]));
755
+ const pending = [...selected];
756
+ while (pending.length) {
757
+ const row = pending.pop();
758
+ if (lower(row.raw_source || row.source) !== 'subagent')
759
+ continue;
760
+ const parentId = str(row.parent_session_id);
761
+ if (!parentId || have.has(parentId))
762
+ continue;
763
+ const parent = byId.get(parentId);
764
+ if (!parent || lower(parent.raw_source || parent.source) !== 'subagent')
765
+ continue;
766
+ selected.push(parent);
767
+ have.add(parentId);
768
+ pending.push(parent);
769
+ }
770
+ return selected;
771
+ });
772
+ return { rows, storage: AGENT_STORAGE_OK };
399
773
  }
400
774
  finally {
401
- db.close();
775
+ finishStateDbRead(db);
402
776
  }
403
777
  }
404
778
  /** Python `agent_session_rows_existing`: batch existence probe; assumes present on any failure so nothing is pruned by mistake. */
@@ -429,12 +803,61 @@ export function agentSessionRowsExisting(dbPath, sessionIds) {
429
803
  return wanted;
430
804
  }
431
805
  finally {
432
- db.close();
806
+ finishStateDbRead(db);
807
+ }
808
+ }
809
+ /** Scheduler admissions are the identity of gateway-fired cron runs, whose session ids stay local-*. */
810
+ function cronAdmissionJobIds(db, terminalOnly = false) {
811
+ const found = new Map();
812
+ const cols = tableColumns(db, 'session_admissions');
813
+ if (!['request_id', 'target_session_id', 'principal_id'].every((col) => cols.has(col)) || (terminalOnly && !cols.has('status')))
814
+ return found;
815
+ // The principal predicate selects the Agent's partial covering index, excluding interactive admissions.
816
+ const rows = db.prepare(`SELECT target_session_id, request_id FROM session_admissions WHERE principal_id = 'cron-owner' AND request_id LIKE 'cron:%'${terminalOnly ? " AND status = 'terminal'" : ''}`).all();
817
+ for (const row of rows) {
818
+ const match = /^cron:([^:]+):([^:]+)$/.exec(row.request_id);
819
+ if (match)
820
+ found.set(row.target_session_id, match[1]);
821
+ }
822
+ return found;
823
+ }
824
+ /** Read-only admission lookup, built once per session listing; older Agent stores have none. */
825
+ export function stateDbCronJobIds(dbPath) {
826
+ if (!existsSync(dbPath))
827
+ return new Map();
828
+ let db;
829
+ try {
830
+ db = openStateDbReadonly(dbPath);
831
+ }
832
+ catch {
833
+ return new Map();
834
+ }
835
+ try {
836
+ return cronAdmissionJobIds(db);
837
+ }
838
+ catch {
839
+ return new Map();
840
+ }
841
+ finally {
842
+ finishStateDbRead(db);
433
843
  }
434
844
  }
845
+ /** Shared job identity rule for completion links and sidebar titles, including legacy job ids with underscores. */
846
+ export function cronSessionJobId(sid, jobIds, admissions) {
847
+ const admitted = admissions.get(sid);
848
+ if (admitted)
849
+ return admitted;
850
+ const legacy = /^cron_(.+)_\d{8}_\d{6}$/.exec(sid);
851
+ if (legacy)
852
+ return legacy[1];
853
+ // Preserve prefix matching for older non-timestamp session ids.
854
+ return [...jobIds].filter((id) => sid.startsWith(`cron_${id}_`)).reduce((best, id) => best === null || id.length > best.length ? id : best, null);
855
+ }
435
856
  /**
436
- * Python `_latest_cron_session_info_for_jobs`: the newest-started cron session for each completed job. A session id
437
- * belongs to the job with the longest matching `cron_{job_id}_` prefix, so `a` never claims `a_b`'s sessions. A
857
+ * Python `_latest_cron_session_info_for_jobs`: the newest-started cron session for each completed job, excluding
858
+ * non-terminal gateway admissions so the next run cannot steal the completion link. A session id
859
+ * belongs to its scheduler admission's job or its legacy timestamp identity. Non-timestamp ids retain the longest
860
+ * matching job prefix, so `a` never claims a known `a_b`'s sessions. A
438
861
  * missing, locked, or unrecognized state.db answers an empty map; callers send no enrichment.
439
862
  */
440
863
  export function latestCronSessionInfo(dbPath, jobIds, completedIds) {
@@ -454,17 +877,23 @@ export function latestCronSessionInfo(dbPath, jobIds, completedIds) {
454
877
  const cols = tableColumns(db, 'sessions');
455
878
  if (!cols.has('id') || !cols.has('source'))
456
879
  return found;
880
+ const admissions = cronAdmissionJobIds(db, true);
457
881
  const count = cols.has('message_count') ? 's.message_count' : 'NULL';
458
882
  const order = cols.has('started_at') ? 'COALESCE(s.started_at, 0) DESC, s.id DESC' : 's.id DESC';
459
- const rows = db.prepare(`SELECT s.id AS id, ${count} AS message_count FROM sessions s WHERE LOWER(COALESCE(s.source, '')) = 'cron' ORDER BY ${order}`).all();
460
- for (const row of rows) {
461
- const sid = typeof row.id === 'string' ? row.id : '';
462
- const owner = ids.filter((id) => sid.startsWith(`cron_${id}_`)).reduce((best, id) => (best === null || id.length > best.length ? id : best), null);
463
- if (owner === null || !wanted.has(owner) || found.has(owner))
464
- continue;
465
- found.set(owner, { session_id: sid, message_count: Number.isFinite(Number(row.message_count)) && row.message_count !== null ? Math.trunc(Number(row.message_count)) : null });
466
- if (found.size === wanted.size)
467
- break;
883
+ const indexed = tableIndexes(db, 'sessions').has('idx_sessions_source_id') ? ' INDEXED BY idx_sessions_source_id' : '';
884
+ const query = db.prepare(`SELECT s.id AS id, ${count} AS message_count FROM sessions s${indexed} WHERE s.source = 'cron' ORDER BY ${order} LIMIT ? OFFSET ?`);
885
+ const batchSize = wanted.size * 8;
886
+ // Stream older rows only if needed: a busy job must not hide another job's completion link.
887
+ for (const [limit, offset] of [[batchSize, 0], [-1, batchSize]]) {
888
+ for (const row of query.iterate(limit, offset)) {
889
+ const sid = typeof row.id === 'string' ? row.id : '';
890
+ const owner = cronSessionJobId(sid, ids, admissions);
891
+ if (owner === null || !wanted.has(owner) || found.has(owner))
892
+ continue;
893
+ found.set(owner, { session_id: sid, message_count: Number.isFinite(Number(row.message_count)) && row.message_count !== null ? Math.trunc(Number(row.message_count)) : null });
894
+ if (found.size === wanted.size)
895
+ return found;
896
+ }
468
897
  }
469
898
  return found;
470
899
  }
@@ -472,18 +901,187 @@ export function latestCronSessionInfo(dbPath, jobIds, completedIds) {
472
901
  return new Map();
473
902
  }
474
903
  finally {
475
- db.close();
904
+ finishStateDbRead(db);
476
905
  }
477
906
  }
478
- /**
479
- * TAL-358: the `sessions.source` owner of each present id, in one chunked read. A missing state.db (or one without a
480
- * `source` column) owns nothing; an unreadable one answers null, so callers fail closed on an unknown owner.
481
- */
482
- export function stateDbSessionSources(dbPath, sessionIds) {
907
+ /** A source label remains display metadata; canonical receipts and ledgers independently retain write ownership. */
908
+ export const CANONICAL_READ_ONLY_REASON = 'This session is owned by the Agent gateway and is display-only in Talaria.';
909
+ export const OWNERSHIP_UNAVAILABLE_REASON = 'Agent session ownership could not be verified. The session is read-only until storage is readable.';
910
+ export const RETIRED_READ_ONLY_REASON = 'The Agent has retired this session identity. Open or import a new conversation to continue.';
911
+ /** Ownership fences outlive the fully validated localLineages history: even a pruned or broken receipt cannot release a writer. */
912
+ function sessionSources(db, cols, ids) {
483
913
  const sources = new Map();
914
+ const canReadRows = cols.has('id') && cols.has('source');
915
+ const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((r) => r.name));
916
+ const walk = canReadRows ? resumeLineageWalker(db, cols) : (sid) => [sid];
917
+ const lineages = new Map(ids.map((id) => [id, walk(id)]));
918
+ const segments = [...new Set([...lineages.values()].flat())];
919
+ const rows = new Map();
920
+ for (let i = 0; canReadRows && i < segments.length; i += 400) {
921
+ const chunk = segments.slice(i, i + 400);
922
+ for (const row of db.prepare(`SELECT id, source, ${cols.has('chat_id') ? 'chat_id' : 'NULL AS chat_id'} FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')})`).all(...chunk))
923
+ rows.set(str(row.id), row);
924
+ }
925
+ const candidates = [...new Set([...segments, ...[...rows.values()].map((r) => str(r.chat_id)).filter(Boolean)])];
926
+ const candidateSet = new Set(candidates);
927
+ const policies = new Map();
928
+ const receiptOwners = new Map();
929
+ const retired = new Set();
930
+ const bindings = new Set();
931
+ const wanted = new Set(segments);
932
+ const policyValue = (value) => {
933
+ try {
934
+ const parsed = JSON.parse(str(value));
935
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
936
+ }
937
+ catch {
938
+ return null;
939
+ }
940
+ };
941
+ const rememberPolicy = (owner, policy) => {
942
+ const entry = policy?.entry;
943
+ const target = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry.session_id : null;
944
+ const lineage = Array.isArray(policy?.lineage) ? policy.lineage : [];
945
+ const aliases = [owner, policy?.session_id, policy?.legacy_session_id, target, ...lineage];
946
+ if (!candidateSet.has(owner) && !aliases.some((id) => typeof id === 'string' && wanted.has(id)))
947
+ return;
948
+ policies.set(owner, policy);
949
+ for (const id of aliases)
950
+ if (typeof id === 'string' && wanted.has(id) && !receiptOwners.has(id))
951
+ receiptOwners.set(id, owner);
952
+ };
953
+ if (tables.has('state_meta')) {
954
+ const prefixes = [LOCAL_POLICY_PREFIX, 'gateway.retired_session.v1.', 'gateway.api.binding.v1.'];
955
+ const keys = candidates.flatMap((owner) => prefixes.map((prefix) => `${prefix}${owner}`));
956
+ const metadata = new Map();
957
+ const read = db.prepare('SELECT key, value FROM state_meta WHERE key IN (SELECT value FROM json_each(?))');
958
+ for (let i = 0; i < keys.length; i += 400) {
959
+ for (const row of read.all(JSON.stringify(keys.slice(i, i + 400))))
960
+ metadata.set(row.key, row.value);
961
+ }
962
+ // Keep receipt precedence in candidate order, independent of SQLite's key ordering.
963
+ for (const owner of candidates) {
964
+ const key = `${LOCAL_POLICY_PREFIX}${owner}`;
965
+ if (metadata.has(key))
966
+ rememberPolicy(owner, policyValue(metadata.get(key)));
967
+ if (metadata.has(`gateway.retired_session.v1.${owner}`))
968
+ retired.add(owner);
969
+ if (metadata.has(`gateway.api.binding.v1.${owner}`))
970
+ bindings.add(owner);
971
+ }
972
+ }
973
+ const ledgerIds = new Set();
974
+ const ledgerCandidates = JSON.stringify(candidates);
975
+ for (const [table, column] of [['session_admissions', 'target_session_id'], ['worker_executions', 'session_id']]) {
976
+ if (!tables.has(table) || !candidates.length)
977
+ continue;
978
+ const hasLineage = tableColumns(db, table).has('lineage_json');
979
+ for (let i = 0; i < candidates.length; i += 400) {
980
+ const chunk = candidates.slice(i, i + 400);
981
+ const marks = chunk.map(() => '?').join(',');
982
+ for (const row of db.prepare(`SELECT ${column} AS id FROM ${table} WHERE ${column} IN (${marks})`).all(...chunk))
983
+ ledgerIds.add(row.id);
984
+ }
985
+ if (hasLineage) {
986
+ // Bind the complete set once so unrelated history never crosses into JavaScript, even across candidate chunks.
987
+ const scan = db.prepare(`SELECT ${column} AS id, lineage_json FROM ${table} WHERE lineage_json IS NOT NULL AND
988
+ (${column} IN (SELECT value FROM json_each(?)) OR EXISTS
989
+ (SELECT 1 FROM json_each(lineage_json) WHERE value IN (SELECT value FROM json_each(?))))`);
990
+ for (const row of scan.iterate(ledgerCandidates, ledgerCandidates)) {
991
+ const lineage = JSON.parse(row.lineage_json);
992
+ if (!Array.isArray(lineage) || !lineage.every((v) => typeof v === 'string'))
993
+ throw new Error('Unreadable Agent admission lineage');
994
+ ledgerIds.add(row.id);
995
+ for (const sid of lineage)
996
+ ledgerIds.add(sid);
997
+ }
998
+ }
999
+ }
1000
+ // Pruned/reset segments can survive only in a receipt, without a row or chat_id to follow.
1001
+ if (tables.has('state_meta') && segments.some((id) => !receiptOwners.has(id) && !ledgerIds.has(id) && !retired.has(id) && !bindings.has(id))) {
1002
+ for (const raw of db.prepare('SELECT key, value FROM state_meta WHERE key GLOB ?').iterate(`${LOCAL_POLICY_PREFIX}*`)) {
1003
+ const policy = policyValue(raw.value);
1004
+ const entry = policy?.entry;
1005
+ const target = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry.session_id : null;
1006
+ if (!policy || typeof policy.session_id !== 'string' || typeof target !== 'string' || (policy.lineage !== undefined && (!Array.isArray(policy.lineage) || !policy.lineage.length || !policy.lineage.every((id) => typeof id === 'string'))))
1007
+ throw new Error('Unreadable Agent ownership receipt');
1008
+ const owner = raw.key.slice(LOCAL_POLICY_PREFIX.length);
1009
+ if (!owner)
1010
+ throw new Error('Unreadable Agent ownership receipt');
1011
+ rememberPolicy(owner, policy);
1012
+ }
1013
+ }
1014
+ const kanban = new Set(kanbanOwnedIds(db, cols, new Map([...lineages].filter(([id]) => rows.has(id)))));
1015
+ for (const id of ids) {
1016
+ const row = rows.get(id);
1017
+ let source = kanban.has(id) ? 'kanban' : str(row?.source);
1018
+ let canonical = false;
1019
+ let retiredId = false;
1020
+ let mode;
1021
+ for (const segment of lineages.get(id) ?? [id]) {
1022
+ const chatId = str(rows.get(segment)?.chat_id);
1023
+ for (const owner of new Set([segment, chatId, receiptOwners.get(segment)].filter((v) => Boolean(v)))) {
1024
+ if (retired.has(owner))
1025
+ retiredId = true;
1026
+ if (ledgerIds.has(owner) || bindings.has(owner))
1027
+ canonical = true;
1028
+ if (!policies.has(owner))
1029
+ continue;
1030
+ const policy = policies.get(owner);
1031
+ // A broken receipt never releases its owner or a physical row naming that owner.
1032
+ canonical = true;
1033
+ const origin = policy?.policy;
1034
+ if (origin && typeof origin === 'object' && !Array.isArray(origin)) {
1035
+ const frozen = origin;
1036
+ if (str(frozen.source))
1037
+ source = str(frozen.source);
1038
+ mode ??= frozen.safe_mode === true ? 'safe' : frozen.ignore_user_config === true ? 'ignore_config' : 'normal';
1039
+ }
1040
+ }
1041
+ }
1042
+ const authority = canonical;
1043
+ // LOCAL is the canonical transport. Missing receipts cannot prove it safe to claim.
1044
+ if (lower(source) === 'local')
1045
+ canonical = true;
1046
+ if (!row && !canonical && !retiredId)
1047
+ continue;
1048
+ const owned = { source, canonical: true, ...(mode ? { execution_mode: mode } : {}) };
1049
+ sources.set(id, retiredId && !authority ? { ...owned, reason: RETIRED_READ_ONLY_REASON } : canonical ? owned : source);
1050
+ }
1051
+ return sources;
1052
+ }
1053
+ /** TAL-1040: the request-id prefix Talaria mints for the diagnostic chats it asks an owner to create; its receipt keeps it. */
1054
+ export const WEB_DIAGNOSTIC_REQUEST_PREFIX = 'talaria-web:';
1055
+ /** Owner ids whose creation receipt carries Talaria's request prefix: chats Web started, shown whatever the external-session setting. */
1056
+ export function stateDbWebDiagnosticIds(dbPath) {
1057
+ if (!existsSync(dbPath))
1058
+ return [];
1059
+ let db;
1060
+ try {
1061
+ db = openStateDbReadonly(dbPath);
1062
+ }
1063
+ catch {
1064
+ return [];
1065
+ }
1066
+ try {
1067
+ if (!tableColumns(db, 'state_meta').has('value'))
1068
+ return [];
1069
+ const rows = db.prepare("SELECT key FROM state_meta WHERE key GLOB ? AND json_valid(value) AND json_extract(value, '$.request_id') GLOB ?")
1070
+ .all(`${LOCAL_POLICY_PREFIX}*`, `${WEB_DIAGNOSTIC_REQUEST_PREFIX}*`);
1071
+ return rows.map((row) => row.key.slice(LOCAL_POLICY_PREFIX.length));
1072
+ }
1073
+ catch {
1074
+ return [];
1075
+ }
1076
+ finally {
1077
+ finishStateDbRead(db);
1078
+ }
1079
+ }
1080
+ /** Batched source/ownership probe. An unreadable authority answers null so callers fail closed. */
1081
+ export function stateDbSessionSources(dbPath, sessionIds) {
484
1082
  const ids = [...new Set([...sessionIds].map((s) => s.trim()).filter(Boolean))];
485
1083
  if (!ids.length || !existsSync(dbPath))
486
- return sources;
1084
+ return new Map();
487
1085
  let db;
488
1086
  try {
489
1087
  db = openStateDbReadonly(dbPath);
@@ -492,22 +1090,82 @@ export function stateDbSessionSources(dbPath, sessionIds) {
492
1090
  return null;
493
1091
  }
494
1092
  try {
495
- const cols = tableColumns(db, 'sessions');
496
- if (!cols.has('id') || !cols.has('source'))
497
- return sources;
498
- for (let i = 0; i < ids.length; i += 500) {
499
- const chunk = ids.slice(i, i + 500);
500
- for (const row of db.prepare(`SELECT id, source FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')})`).all(...chunk))
501
- sources.set(row.id, str(row.source));
502
- }
503
- return sources;
1093
+ return sessionSources(db, tableColumns(db, 'sessions'), ids);
504
1094
  }
505
1095
  catch {
506
1096
  return null;
507
1097
  }
508
1098
  finally {
509
- db.close();
1099
+ finishStateDbRead(db);
1100
+ }
1101
+ }
1102
+ /** The same marker parser serves conservative history boundaries and exact-parent ownership walks. */
1103
+ function explicitForkMarkers(row, includeReset) {
1104
+ let cfg = row.model_config;
1105
+ if (typeof cfg === 'string') {
1106
+ try {
1107
+ cfg = JSON.parse(cfg);
1108
+ }
1109
+ catch {
1110
+ return [];
1111
+ }
510
1112
  }
1113
+ if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
1114
+ return [];
1115
+ const c = cfg;
1116
+ return [c._branched_from, c._delegate_from, ...(includeReset ? [c._reset_from] : [])].filter((m) => m !== undefined && m !== null);
1117
+ }
1118
+ /** Python `_is_explicit_fork_child_row`: a tool child, or a branch/delegate (`includeReset`: also reset) fork of its parent. */
1119
+ function isExplicitForkChild(row, includeReset) {
1120
+ if (str(row.source) === 'tool')
1121
+ return true;
1122
+ const markers = explicitForkMarkers(row, includeReset);
1123
+ const parentId = row.parent_session_id;
1124
+ return parentId ? markers.includes(parentId) : markers.length > 0;
1125
+ }
1126
+ /**
1127
+ * Python `_resume_lineage_ids`: `sid` and the compression ancestors a resume loads, root first. Each hop's parent ended
1128
+ * `compression` and the child is no branch, reset, delegate or tool fork. The walk stops at a cycle or
1129
+ * `RESUME_LINEAGE_CAP` ids; without the lineage columns it is `[sid]`.
1130
+ */
1131
+ function resumeLineageWalker(db, cols) {
1132
+ if (!['parent_session_id', 'end_reason'].every((c) => cols.has(c)))
1133
+ return (sid) => [sid];
1134
+ const select = db.prepare(`SELECT id, source, parent_session_id, end_reason, ${cols.has('model_config') ? 'model_config' : 'NULL AS model_config'} FROM sessions WHERE id = ?`);
1135
+ return (sid) => {
1136
+ const chain = [sid];
1137
+ let current = select.get(sid);
1138
+ if (!current || isExplicitForkChild(current, false))
1139
+ return chain;
1140
+ while (chain.length < RESUME_LINEAGE_CAP) {
1141
+ const parentId = str(current.parent_session_id);
1142
+ if (!parentId || chain.includes(parentId) || isExplicitForkChild(current, true))
1143
+ break;
1144
+ const parent = select.get(parentId);
1145
+ if (str(parent?.end_reason) !== 'compression')
1146
+ break;
1147
+ chain.push(parentId);
1148
+ current = parent;
1149
+ }
1150
+ return chain.reverse();
1151
+ };
1152
+ }
1153
+ /**
1154
+ * TAL-793 (Agent `is_kanban_owned_session`): the `ids` whose compression lineage has a `kanban` `source` or
1155
+ * `created_source` (schema v31; `source` alone decides before it), a Kanban worker's transcript only its dispatcher continues.
1156
+ */
1157
+ function kanbanOwnedIds(db, cols, lineages) {
1158
+ if (!lineages.size)
1159
+ return [];
1160
+ const segments = [...new Set([...lineages.values()].flat())];
1161
+ const match = cols.has('created_source') ? "(source = 'kanban' OR created_source = 'kanban')" : "source = 'kanban'";
1162
+ const kanban = new Set();
1163
+ for (let i = 0; i < segments.length; i += 500) {
1164
+ const chunk = segments.slice(i, i + 500);
1165
+ for (const row of db.prepare(`SELECT id FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')}) AND ${match}`).all(...chunk))
1166
+ kanban.add(row.id);
1167
+ }
1168
+ return [...lineages].filter(([, lineage]) => lineage.some((sid) => kanban.has(sid))).map(([id]) => id);
511
1169
  }
512
1170
  /** Python `state_db_has_session`: true only when `sid` is a row of the sessions table; a missing or unreadable db is false. */
513
1171
  export function stateDbHasSession(dbPath, sid) {
@@ -530,7 +1188,33 @@ export function stateDbHasSession(dbPath, sid) {
530
1188
  return false;
531
1189
  }
532
1190
  finally {
533
- db.close();
1191
+ finishStateDbRead(db);
1192
+ }
1193
+ }
1194
+ /** True when a session has a live message; never materializes its transcript or compaction archive. */
1195
+ export function stateDbSessionHasMessages(dbPath, sid) {
1196
+ const id = sid.trim();
1197
+ if (!id || !existsSync(dbPath))
1198
+ return false;
1199
+ let db;
1200
+ try {
1201
+ db = openStateDbReadonly(dbPath);
1202
+ }
1203
+ catch {
1204
+ return false;
1205
+ }
1206
+ try {
1207
+ const cols = tableColumns(db, 'messages');
1208
+ if (!cols.has('session_id'))
1209
+ return false;
1210
+ const active = cols.has('active') ? ' AND (active IS NULL OR active != 0)' : '';
1211
+ return db.prepare(`SELECT 1 FROM messages WHERE session_id = ?${active} LIMIT 1`).get(id) !== undefined;
1212
+ }
1213
+ catch {
1214
+ return false;
1215
+ }
1216
+ finally {
1217
+ finishStateDbRead(db);
534
1218
  }
535
1219
  }
536
1220
  /**
@@ -541,7 +1225,7 @@ export function stateDbHasSession(dbPath, sid) {
541
1225
  export function cheapChangeFingerprint(dbPath, onError) {
542
1226
  try {
543
1227
  const parts = [];
544
- const db = openStateDbReadonly(dbPath);
1228
+ const db = openStateDbReadonly(dbPath, STATE_DB_FINGERPRINT_BUSY_TIMEOUT_MS);
545
1229
  try {
546
1230
  for (const table of ['sessions', 'messages']) {
547
1231
  try {
@@ -553,7 +1237,7 @@ export function cheapChangeFingerprint(dbPath, onError) {
553
1237
  }
554
1238
  }
555
1239
  finally {
556
- db.close();
1240
+ finishStateDbRead(db);
557
1241
  }
558
1242
  for (const path of [dbPath, `${dbPath}-wal`]) {
559
1243
  try {
@@ -585,22 +1269,28 @@ export function cheapChangeFingerprint(dbPath, onError) {
585
1269
  return null;
586
1270
  }
587
1271
  }
588
- let listingWorker = null;
1272
+ const STATE_DB_WORKER_LANES = ['listing', 'authority', 'session'];
1273
+ const listingWorkers = new Map();
1274
+ const listingReadScope = new AsyncLocalStorage();
1275
+ /** Keep a sidebar phase's nested reads on its lane without changing interactive callers' defaults. */
1276
+ export function withStateDbListingReads(read) { return listingReadScope.run('listing', read); }
589
1277
  let nextCallId = 0;
590
- /** Set while `stopStateDbWorker` waits for the worker to exit; calls then reject instead of starting another worker. */
1278
+ /** Set until every worker exits during shutdown; calls then reject instead of starting another worker. */
591
1279
  let stoppingWorker = false;
592
1280
  /** Reject every call still waiting on `current` and forget it, so the next call starts a fresh worker. */
593
1281
  function retireListingWorker(current, error) {
594
- if (listingWorker === current)
595
- listingWorker = null;
1282
+ for (const [lane, worker] of listingWorkers)
1283
+ if (worker === current)
1284
+ listingWorkers.delete(lane);
596
1285
  for (const call of current.pending.values())
597
1286
  call.reject(error);
598
1287
  current.pending.clear();
599
1288
  }
600
- /** The lazily started, long-lived listing worker; requests run one at a time in arrival order. */
601
- function startedListingWorker() {
602
- if (listingWorker)
603
- return listingWorker;
1289
+ /** Each lane lazily starts one long-lived worker and keeps its own FIFO and read-only handle cache. */
1290
+ function startedListingWorker(lane) {
1291
+ const existing = listingWorkers.get(lane);
1292
+ if (existing)
1293
+ return existing;
604
1294
  // Source runs (vitest) load the TypeScript worker; the build runs the compiled one beside this module.
605
1295
  const ext = import.meta.url.endsWith('.ts') ? 'ts' : 'js';
606
1296
  const current = { worker: new Worker(new URL(`./state-db-worker.${ext}`, import.meta.url)), pending: new Map() };
@@ -616,20 +1306,26 @@ function startedListingWorker() {
616
1306
  });
617
1307
  current.worker.on('error', (error) => { retireListingWorker(current, error); });
618
1308
  current.worker.on('exit', (code) => { retireListingWorker(current, new Error(`state.db worker exited with code ${String(code)}`)); });
619
- listingWorker = current;
1309
+ listingWorkers.set(lane, current);
620
1310
  return current;
621
1311
  }
622
- function callListingWorker(call) {
1312
+ function callListingWorker(call, lane) {
623
1313
  // A multi-read caller the stop interrupted goes on to its next read; that read must not restart the worker.
624
1314
  if (stoppingWorker)
625
1315
  return Promise.reject(new Error('state.db worker stopped'));
626
- const current = startedListingWorker();
1316
+ const target = lane ?? listingReadScope.getStore() ?? (call.op === 'session' ? SESSION_READ_LANES[call.read] : call.op === 'sources' || call.op === 'existing' ? 'authority' : 'listing');
1317
+ const current = startedListingWorker(target);
627
1318
  const id = ++nextCallId;
1319
+ const timing = startupTimingScope.getStore();
628
1320
  return new Promise((resolve, reject) => {
629
- current.pending.set(id, { resolve, reject });
1321
+ current.pending.set(id, { resolve: (reply) => {
1322
+ if (reply.timing)
1323
+ timing?.workerRead(target, call.op === 'session' ? call.read : call.op, reply.timing);
1324
+ resolve(reply);
1325
+ }, reject });
630
1326
  // A call in flight keeps the process alive until it settles; an idle worker never does.
631
1327
  current.worker.ref();
632
- current.worker.postMessage({ ...call, id, delayMs: readDelayMs });
1328
+ current.worker.postMessage({ ...call, id, delayMs: readDelayMs, queuedAtNs: process.hrtime.bigint() });
633
1329
  });
634
1330
  }
635
1331
  /** `readImportableAgentSessionRows` on the listing worker, so a slow `state.db` never blocks the event loop. Rejects when the worker fails. */
@@ -642,6 +1338,15 @@ export async function readImportableAgentSessionRowsAsync(dbPath, opts = {}) {
642
1338
  throw new Error(reply.error);
643
1339
  return reply.value;
644
1340
  }
1341
+ /** One worker crossing for all sidebar windows and their storage health. */
1342
+ export async function readImportableAgentSessionRowBatchesAsync(dbPath, queries) {
1343
+ const reply = await callListingWorker({ op: 'rowBatches', dbPath, queries: queries.map((opts) => ({ ...opts, log: undefined })) });
1344
+ for (const line of reply.logs)
1345
+ queries[0]?.log?.(line);
1346
+ if (reply.error !== undefined)
1347
+ throw new Error(reply.error);
1348
+ return reply.value;
1349
+ }
645
1350
  /** `cheapChangeFingerprint` on the listing worker; a worker failure reports as a failed change check. */
646
1351
  export async function cheapChangeFingerprintAsync(dbPath, onError) {
647
1352
  let reply;
@@ -656,8 +1361,19 @@ export async function cheapChangeFingerprintAsync(dbPath, onError) {
656
1361
  onError?.(reason, new Error(message));
657
1362
  return reply.value;
658
1363
  }
659
- /** `stateDbSessionSources` on the listing worker; a worker failure is an unreadable state.db (null), so owners stay locked. */
1364
+ /** `stateDbStorage` on the listing worker; a worker failure is `unavailable`. */
1365
+ export async function stateDbStorageAsync(dbPath) {
1366
+ try {
1367
+ return (await callListingWorker({ op: 'storage', dbPath })).value;
1368
+ }
1369
+ catch (error) {
1370
+ return stateDbReadFailure(error);
1371
+ }
1372
+ }
1373
+ /** `stateDbSessionSources` on the authority worker; a worker failure is an unreadable state.db (null), so owners stay locked. Without a state.db it answers here, opening nothing. */
660
1374
  export async function stateDbSessionSourcesAsync(dbPath, sessionIds) {
1375
+ if (!existsSync(dbPath))
1376
+ return stateDbSessionSources(dbPath, sessionIds);
661
1377
  try {
662
1378
  return (await callListingWorker({ op: 'sources', dbPath, ids: [...sessionIds] })).value;
663
1379
  }
@@ -665,7 +1381,16 @@ export async function stateDbSessionSourcesAsync(dbPath, sessionIds) {
665
1381
  return null;
666
1382
  }
667
1383
  }
668
- /** `agentSessionRowsExisting` on the listing worker; a worker failure assumes every row present, so nothing is pruned. */
1384
+ /** `stateDbWebDiagnosticIds` on the listing worker; a failed read lists none, so the setting still governs every other row. */
1385
+ export async function stateDbWebDiagnosticIdsAsync(dbPath) {
1386
+ try {
1387
+ return (await callListingWorker({ op: 'webDiagnostic', dbPath })).value;
1388
+ }
1389
+ catch {
1390
+ return [];
1391
+ }
1392
+ }
1393
+ /** `agentSessionRowsExisting` on the authority worker; a worker failure assumes every row present, so nothing is pruned. */
669
1394
  export async function agentSessionRowsExistingAsync(dbPath, sessionIds) {
670
1395
  const ids = [...sessionIds];
671
1396
  try {
@@ -675,19 +1400,60 @@ export async function agentSessionRowsExistingAsync(dbPath, sessionIds) {
675
1400
  return new Set(ids.map((s) => s.trim()).filter(Boolean));
676
1401
  }
677
1402
  }
678
- /** Test hook: the listing worker exits abruptly while this call is in flight. */
679
- export function crashStateDbWorkerForTests() {
680
- return callListingWorker({ op: 'crash' });
1403
+ /**
1404
+ * TAL-923: the single-session readers behind chat start, session detail, and the mutation gates, run on the worker.
1405
+ * TAL-1162: and the transcript reads behind turn history, settlement, and the detail's page tail.
1406
+ * TAL-1256: shared cron admission identities and completion links use this same dispatcher.
1407
+ */
1408
+ export const SESSION_READS = { stateDbHasSession, stateDbSessionHasMessages, insightsSessionRows, countConversationRounds, stateDbTailToolContent, stateDbLineageReport, stateDbCronJobIds, latestCronSessionInfo, stateDbLocalTargetRead, stateDbSessionRow, stateDbSessionMessages, stateDbCompressionLineage, stateDbSessionRead, stateDbSessionTailRead, stateDbSettlementRead };
1409
+ /** Exhaustive policy: authority never waits for transcript materialization; sidebar-scoped probes stay on listing. */
1410
+ const SESSION_READ_LANES = {
1411
+ stateDbHasSession: 'authority', stateDbSessionHasMessages: 'authority', stateDbLocalTargetRead: 'authority', stateDbSessionRow: 'authority',
1412
+ insightsSessionRows: 'session', countConversationRounds: 'session', stateDbTailToolContent: 'session', stateDbLineageReport: 'session',
1413
+ stateDbCronJobIds: 'session', latestCronSessionInfo: 'session', stateDbSessionMessages: 'session', stateDbCompressionLineage: 'session',
1414
+ stateDbSessionRead: 'session', stateDbSessionTailRead: 'session', stateDbSettlementRead: 'session',
1415
+ };
1416
+ /**
1417
+ * A `SESSION_READS` reader on its authority or transcript lane, so expensive reads cannot hold up authority. Each reader
1418
+ * already answers a failed read with `fallback`'s value; a worker failure answers it too. Without a state.db the reader
1419
+ * answers here, as it opens nothing then.
1420
+ */
1421
+ export async function stateDbSessionReadAsync(fallback, read, ...args) {
1422
+ if (!existsSync(args[0]))
1423
+ return SESSION_READS[read](...args);
1424
+ try {
1425
+ const reply = await callListingWorker({ op: 'session', read, args });
1426
+ return reply.error === undefined ? reply.value : fallback;
1427
+ }
1428
+ catch {
1429
+ return fallback;
1430
+ }
1431
+ }
1432
+ /** Test hook: crash one lane, or all when omitted, while its calls are in flight. */
1433
+ export function crashStateDbWorkerForTests(lane) {
1434
+ return Promise.all((lane ? [lane] : STATE_DB_WORKER_LANES).map((target) => callListingWorker({ op: 'crash' }, target)));
681
1435
  }
682
- /** Stop the listing worker (server shutdown). Calls in flight reject; a later call starts a new worker. */
1436
+ /** Stop every worker (server shutdown). Calls in flight reject; later calls start fresh workers. */
683
1437
  export async function stopStateDbWorker() {
684
- const current = listingWorker;
685
- if (!current)
1438
+ closeStateDbs();
1439
+ const workers = [...listingWorkers.values()];
1440
+ if (!workers.length)
686
1441
  return;
687
1442
  stoppingWorker = true;
688
1443
  try {
689
- retireListingWorker(current, new Error('state.db worker stopped'));
690
- await current.worker.terminate();
1444
+ await Promise.all(workers.map(async (current) => {
1445
+ retireListingWorker(current, new Error('state.db worker stopped'));
1446
+ const exited = new Promise((resolve) => { current.worker.once('exit', () => { resolve(); }); });
1447
+ // Idle workers close their cache through process.exit; a blocked read still cannot delay shutdown.
1448
+ const force = setTimeout(() => { void current.worker.terminate(); }, 100);
1449
+ try {
1450
+ current.worker.postMessage({ op: 'stop', id: 0, delayMs: 0 });
1451
+ await exited;
1452
+ }
1453
+ finally {
1454
+ clearTimeout(force);
1455
+ }
1456
+ }));
691
1457
  }
692
1458
  finally {
693
1459
  stoppingWorker = false;
@@ -696,16 +1462,23 @@ export async function stopStateDbWorker() {
696
1462
  // ── message reader ────────────────────────────────────────────────────────
697
1463
  const STATE_DB_CONTENT_JSON_PREFIX = '\0json:';
698
1464
  // `display_kind` / `display_metadata`: the Agent's own tag for rows it writes itself (steers, delivered notifications; TAL-371).
699
- const OPTIONAL_MESSAGE_COLUMNS = ['tool_call_id', 'tool_calls', 'tool_name', 'reasoning', 'reasoning_details', 'codex_reasoning_items', 'reasoning_content', 'codex_message_items', 'api_content', 'display_kind', 'display_metadata'];
700
- const JSON_MESSAGE_COLUMNS = new Set(['tool_calls', 'reasoning_details', 'codex_reasoning_items', 'codex_message_items', 'display_metadata']);
1465
+ const OPTIONAL_MESSAGE_COLUMNS = ['tool_call_id', 'tool_calls', 'tool_name', 'reasoning', 'reasoning_details', 'codex_reasoning_items', 'reasoning_content', 'codex_message_items', 'api_content', 'display_kind', 'display_metadata', 'absorbed_message_uids', '_compressed_summary'];
1466
+ const JSON_MESSAGE_COLUMNS = new Set(['tool_calls', 'reasoning_details', 'codex_reasoning_items', 'codex_message_items', 'display_metadata', 'absorbed_message_uids']);
701
1467
  /** Python `_decode_state_db_content`: the Agent's sentinel-prefixed structured content, left untouched otherwise. */
702
1468
  function decodeStateDbContent(value) {
703
1469
  const text = Buffer.isBuffer(value) ? value.toString('utf8') : value;
704
1470
  if (typeof text !== 'string' || !text.startsWith(STATE_DB_CONTENT_JSON_PREFIX))
705
1471
  return text;
706
1472
  try {
707
- const decoded = JSON.parse(text.slice(STATE_DB_CONTENT_JSON_PREFIX.length));
708
- return Array.isArray(decoded) ? decoded : text;
1473
+ const payload = text.slice(STATE_DB_CONTENT_JSON_PREFIX.length);
1474
+ let unsafeInteger = false;
1475
+ const decoded = JSON.parse(payload, (_key, value, context) => {
1476
+ if (typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context?.source ?? ''))
1477
+ unsafeInteger = true;
1478
+ return value;
1479
+ });
1480
+ // Python keeps arbitrary-precision integers. Retain the JSON text rather than silently rounding any of them.
1481
+ return unsafeInteger ? payload : decoded;
709
1482
  }
710
1483
  catch {
711
1484
  return text;
@@ -721,20 +1494,39 @@ function jsonLoadsIfString(value) {
721
1494
  return value;
722
1495
  }
723
1496
  }
1497
+ // Agent v0.22.3's ordinary kept-tail handoff. Only its explicit flag and exact wrapper prove a carrier.
1498
+ const PRIOR_CONTEXT_HEADER = '[PRIOR CONTEXT — for reference only; not a new message]';
1499
+ const COMPACTION_SUMMARY_DELIMITER = '\n\n[END OF PRIOR CONTEXT — COMPACTION SUMMARY BELOW]\n\n';
1500
+ function compactionCarrier(row) {
1501
+ if (row._compressed_summary !== 1 && row._compressed_summary !== true)
1502
+ return null;
1503
+ const content = decodeStateDbContent(row.content);
1504
+ if ((row.role !== 'user' && row.role !== 'assistant') || typeof content !== 'string' || !content.startsWith(`${PRIOR_CONTEXT_HEADER}\n`))
1505
+ return null;
1506
+ const boundary = content.indexOf(COMPACTION_SUMMARY_DELIMITER, PRIOR_CONTEXT_HEADER.length);
1507
+ if (boundary < 0)
1508
+ return null;
1509
+ // Remove only the separators the Agent added; preserve the kept reply's own whitespace.
1510
+ const prior = content.slice(PRIOR_CONTEXT_HEADER.length + 1, boundary);
1511
+ const summary = content.slice(boundary + COMPACTION_SUMMARY_DELIMITER.length).trim();
1512
+ return prior && summary ? { prior, summary } : null;
1513
+ }
724
1514
  /** Python `_project_state_db_message`: one row → WebUI message (`tool_name → name`, empty optionals omitted). */
725
1515
  function projectStateDbMessage(row, hasId) {
726
- const msg = { role: row.role, content: decodeStateDbContent(row.content), timestamp: row.timestamp };
1516
+ const msg = { role: row.role, content: compactionCarrier(row)?.prior ?? decodeStateDbContent(row.content), timestamp: row.timestamp };
727
1517
  for (const col of OPTIONAL_MESSAGE_COLUMNS) {
728
- if (!(col in row))
1518
+ if (col === '_compressed_summary' || !(col in row))
729
1519
  continue;
730
1520
  const value = row[col];
731
1521
  if (value === null || value === undefined || value === '')
732
1522
  continue;
733
- msg[col] = JSON_MESSAGE_COLUMNS.has(col) ? jsonLoadsIfString(value) : value;
1523
+ msg[col === 'absorbed_message_uids' ? '_absorbed_message_uids' : col] = JSON_MESSAGE_COLUMNS.has(col) ? jsonLoadsIfString(value) : value;
734
1524
  }
735
1525
  // TAL-493: the merge tells rows committed after the session's last read by this id.
736
1526
  if (hasId && row.id !== null && row.id !== undefined)
737
1527
  msg._state_db_row_id = row.id;
1528
+ if (typeof row.message_uid === 'string' && row.message_uid.length)
1529
+ msg._state_db_message_uid = row.message_uid;
738
1530
  if (msg.role === 'tool' && msg.tool_name && !msg.name)
739
1531
  msg.name = msg.tool_name;
740
1532
  return msg;
@@ -753,6 +1545,76 @@ function stateDbGeneration(dbPath) {
753
1545
  return null;
754
1546
  }
755
1547
  }
1548
+ // Agent v0.21.6's exact in-flight request wrapper. Unknown rewrites are not copy evidence.
1549
+ const INFLIGHT_REPLAY_HEADER = '[STILL IN PROGRESS — this is the active request, restated after the compaction boundary because it was not finished yet. Continue it; do not start over.]';
1550
+ /** Compare the kept payload after the Agent's exact request/carrier wrappers and stale api_content are removed. */
1551
+ function replayPayload(row) {
1552
+ const message = projectStateDbMessage(row, false);
1553
+ delete message.timestamp;
1554
+ const copyKey = JSON.stringify(message);
1555
+ let content = message.content;
1556
+ let replay = false;
1557
+ if (row.role === 'user') {
1558
+ if (typeof content === 'string') {
1559
+ replay = content.startsWith(`${INFLIGHT_REPLAY_HEADER}\n`);
1560
+ content = (replay ? content.slice(INFLIGHT_REPLAY_HEADER.length + 1) : content).trim();
1561
+ }
1562
+ else if (Array.isArray(content)) {
1563
+ // Multimodal replays prepend a text block and retain all original parts, including media.
1564
+ const parts = content;
1565
+ let start = 0;
1566
+ while (start < parts.length) {
1567
+ const first = parts[start];
1568
+ if (!first || typeof first !== 'object' || Array.isArray(first))
1569
+ break;
1570
+ const part = first;
1571
+ if (part.type !== 'text' || part.text !== `${INFLIGHT_REPLAY_HEADER}\n` || Object.keys(part).some((k) => k !== 'type' && k !== 'text'))
1572
+ break;
1573
+ start += 1;
1574
+ }
1575
+ replay = start > 0;
1576
+ content = parts.slice(start);
1577
+ }
1578
+ }
1579
+ message.content = content;
1580
+ delete message.api_content; // _reappend_inflight_user_task drops this stale model-facing sidecar.
1581
+ return { copyKey, replayKey: JSON.stringify(message), replay, carrier: compactionCarrier(row)?.summary ?? null };
1582
+ }
1583
+ /**
1584
+ * Compare adjacent generations, never all rows of one UID with its oldest archive. The archive retains
1585
+ * authored payload changes even after active/compacted flags change. Only unchanged timestamp copies or
1586
+ * exact request restatements inherit identity; edits (including A -> B -> A) start at their own row ID.
1587
+ */
1588
+ function replayIdentities(db, chain, rows, available) {
1589
+ const identities = new Map();
1590
+ // Tool-result text can be pruned without changing its tool-call identity; retain the existing rule.
1591
+ const uids = [...new Set(rows.filter((row) => row.role !== 'tool').map((row) => row.message_uid).filter((uid) => typeof uid === 'string' && uid.length > 0))];
1592
+ if (!uids.length)
1593
+ return identities;
1594
+ const liveIds = new Set(rows.map((row) => row.id));
1595
+ const previous = new Map();
1596
+ const columns = ['id', 'message_uid', 'active', 'compacted', 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
1597
+ // One ordered scan of the selected sessions, filtering against a materialized UID set. Stream the rows;
1598
+ // memory holds only the preceding payload per live UID, not the accumulated archive. NULL/nonstandard
1599
+ // active flags take exactly the same path and do not depend on the Agent's partial active=1 UID index.
1600
+ const history = db.prepare(`SELECT ${columns.join(', ')} FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')}) AND message_uid IN (SELECT value FROM json_each(?)) ORDER BY id`);
1601
+ for (const row of history.iterate(...chain, JSON.stringify(uids))) {
1602
+ const uid = row.message_uid;
1603
+ const payload = replayPayload(row);
1604
+ const prior = previous.get(uid);
1605
+ const copy = payload.copyKey === prior?.copyKey && row.timestamp === prior.timestamp;
1606
+ const restatement = payload.replayKey === prior?.replayKey && ((payload.replay && (prior.active !== 0 || prior.compacted === 1)) ||
1607
+ (payload.carrier && row.timestamp === prior.timestamp));
1608
+ const identity = prior && (copy || restatement) ? prior.identity : Number(row.id);
1609
+ const changed = prior ? (copy || restatement ? prior.changed : true) : false;
1610
+ const sourceKey = prior && (copy || restatement) ? prior.sourceKey : stateDbMessageFingerprint(projectStateDbMessage(row, false));
1611
+ const summaryId = payload.carrier && payload.carrier === prior?.carrier && row.timestamp === prior.timestamp ? prior.summaryId : Number(row.id);
1612
+ if (prior && row.role !== 'tool' && liveIds.has(row.id))
1613
+ identities.set(Number(row.id), { id: identity, changed, sourceKey, summaryId });
1614
+ previous.set(uid, { ...payload, identity, changed, sourceKey, summaryId, timestamp: row.timestamp, active: row.active, compacted: row.compacted });
1615
+ }
1616
+ return identities;
1617
+ }
756
1618
  export function stateDbSessionMessages(dbPath, sid, opts = {}) {
757
1619
  return stateDbSessionRead(dbPath, sid, opts).rows;
758
1620
  }
@@ -761,7 +1623,24 @@ export function stateDbSessionMessages(dbPath, sid, opts = {}) {
761
1623
  * (TAL-493: an empty id-capable read is a baseline of 0; a missing database or a failed read says nothing).
762
1624
  */
763
1625
  export function stateDbSessionRead(dbPath, sid, opts = {}) {
764
- const none = { rows: [], idCapable: false, ok: false };
1626
+ return readStateDbSession(dbPath, sid, opts);
1627
+ }
1628
+ /**
1629
+ * A settlement's reads in one request: the transcript under `recorded` (the session's `state_db_lineage`), the lineage
1630
+ * the Agent's compression rotations give it now, and, when that differs from `recorded`, the transcript under it.
1631
+ */
1632
+ export function stateDbSettlementRead(dbPath, sid, recorded) {
1633
+ const read = stateDbSessionRead(dbPath, sid, { stitch: false, lineage: recorded?.[0] === sid ? recorded : null });
1634
+ const lineage = stateDbCompressionLineage(dbPath, sid);
1635
+ const moved = JSON.stringify(lineage) !== JSON.stringify(recorded ?? [sid]);
1636
+ return { ...read, lineage, rotated: moved ? stateDbSessionRead(dbPath, sid, { stitch: false, lineage: lineage.length > 1 && lineage[0] === sid ? lineage : null }) : null };
1637
+ }
1638
+ /** The same snapshot and provenance projection, restricted to physical rows past the seen marker. */
1639
+ export function stateDbSessionTailRead(dbPath, sid, afterId, opts = {}) {
1640
+ return readStateDbSession(dbPath, sid, { ...opts, stitch: false, afterId });
1641
+ }
1642
+ function readStateDbSession(dbPath, sid, opts) {
1643
+ const none = { rows: [], idCapable: false, ok: false, reason: null, generation: null };
765
1644
  const id = sid.trim();
766
1645
  if (!id || !existsSync(dbPath))
767
1646
  return none;
@@ -769,24 +1648,26 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
769
1648
  try {
770
1649
  db = openStateDbReadonly(dbPath);
771
1650
  }
772
- catch {
773
- return none;
1651
+ catch (error) {
1652
+ return { ...none, reason: stateDbReadFailure(error).reason };
774
1653
  }
775
1654
  try {
776
1655
  const available = tableColumns(db, 'messages');
777
1656
  if (!['role', 'content', 'timestamp'].every((c) => available.has(c)))
778
1657
  return { ...none, ok: available.size > 0 };
779
1658
  const hasId = available.has('id');
780
- const selected = [...(hasId ? ['id'] : []), 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
1659
+ const selected = [...(hasId ? ['id'] : []), ...(available.has('message_uid') ? ['message_uid'] : []), 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
781
1660
  // TAL-529: an explicit lineage (the session and its compression continuations) replaces the parent walk.
782
- const chain = opts.lineage?.length ? [...opts.lineage] : [id];
783
- if (!opts.lineage?.length && (opts.stitch ?? true)) {
1661
+ const local = localLineages(db, id).get(id);
1662
+ const chain = local ? (opts.stitch === false ? [local.ids.at(-1) ?? id] : [...local.context]) : opts.lineage?.length ? [...opts.lineage] : [id];
1663
+ if (!local && !opts.lineage?.length && (opts.stitch ?? true)) {
784
1664
  const sessionCols = tableColumns(db, 'sessions');
785
1665
  if (['parent_session_id', 'end_reason', 'started_at', 'source'].every((c) => sessionCols.has(c))) {
786
- const select = db.prepare('SELECT id, source, started_at, parent_session_id, ended_at, end_reason FROM sessions WHERE id = ?');
1666
+ const extra = ['model_config', 'session_source'].map((c) => sessionCols.has(c) ? c : `NULL AS ${c}`).join(', ');
1667
+ const select = db.prepare(`SELECT id, source, started_at, parent_session_id, ended_at, end_reason, ${extra} FROM sessions WHERE id = ?`);
787
1668
  let current = select.get(id);
788
1669
  const seen = new Set([id]);
789
- for (let hop = 0; hop < 20 && current; hop += 1) {
1670
+ for (let hop = 0; hop < COMPRESSION_CHAIN_CAP && current; hop += 1) {
790
1671
  const parentId = str(current.parent_session_id);
791
1672
  if (!parentId || seen.has(parentId))
792
1673
  break;
@@ -800,9 +1681,13 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
800
1681
  }
801
1682
  }
802
1683
  const activeClause = available.has('active') ? ' AND (active IS NULL OR active != 0)' : '';
1684
+ if (opts.afterId !== undefined && !hasId)
1685
+ return { ...none, ok: true };
1686
+ const range = opts.afterId === undefined ? '' : ' AND id > ?';
1687
+ const rangeArgs = opts.afterId === undefined ? [] : [opts.afterId];
803
1688
  const order = hasId ? 'id' : 'timestamp';
804
- const rows = db.prepare(`SELECT ${selected.join(', ')}, session_id FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')})${activeClause} ORDER BY ${order} ASC`).all(...chain);
805
- const projected = rows.map((row) => projectStateDbMessage(row, hasId));
1689
+ const rows = db.prepare(`SELECT ${selected.join(', ')}, session_id FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')})${activeClause}${range} ORDER BY ${order} ASC`).all(...chain, ...rangeArgs);
1690
+ let projected = rows.map((row) => projectStateDbMessage(row, hasId));
806
1691
  // TAL-709: an in-place compaction archives the live rows and re-inserts each as a copy that keeps its role and timestamp
807
1692
  // (content may be pruned). A copy takes the id of the oldest archived row it copies, so the merge sees a row it has
808
1693
  // already read instead of a new one. Only a compaction archive (`compacted = 1`) vouches for a changed copy: a rewound
@@ -813,7 +1698,7 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
813
1698
  // compactions it holds.
814
1699
  const sameCall = available.has('tool_call_id') ? " AND COALESCE(a.tool_call_id, '') = COALESCE(m.tool_call_id, '')" : '';
815
1700
  const vouched = [available.has('compacted') ? 'a.compacted = 1' : '', available.has('tool_call_id') ? 'm.tool_call_id IS NOT NULL' : '', 'a.content = m.content'].filter(Boolean).join(' OR ');
816
- const twins = db.prepare(`SELECT m.id AS id, MIN(a.id) AS twin FROM messages m JOIN messages a ON a.session_id = m.session_id AND a.active = 0 AND a.role = m.role AND a.timestamp = m.timestamp AND a.id < m.id${sameCall} WHERE m.session_id IN (${chain.map(() => '?').join(', ')}) AND (m.active IS NULL OR m.active != 0) AND (${vouched}) GROUP BY m.id`).all(...chain);
1701
+ const twins = db.prepare(`SELECT m.id AS id, MIN(a.id) AS twin FROM messages m JOIN messages a ON a.session_id = m.session_id AND a.active = 0 AND a.role = m.role AND a.timestamp = m.timestamp AND a.id < m.id${sameCall} WHERE m.session_id IN (${chain.map(() => '?').join(', ')}) AND (m.active IS NULL OR m.active != 0) AND (${vouched})${opts.afterId === undefined ? '' : ' AND m.id > ?'} GROUP BY m.id`).all(...chain, ...rangeArgs);
817
1702
  const twinOf = new Map(twins.map((row) => [row.id, row.twin]));
818
1703
  for (const [i, row] of rows.entries()) {
819
1704
  const twin = twinOf.get(row.id);
@@ -821,25 +1706,66 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
821
1706
  projected[i]._state_db_row_id = twin;
822
1707
  }
823
1708
  }
1709
+ let identities;
1710
+ if (hasId && ['message_uid', 'active', 'compacted'].every((c) => available.has(c))) {
1711
+ identities = replayIdentities(db, local?.context ?? chain, rows, available);
1712
+ for (const [i, row] of rows.entries()) {
1713
+ const identity = identities.get(Number(row.id));
1714
+ // A known changed generation overrides even a coincidental legacy timestamp twin.
1715
+ if (identity !== undefined) {
1716
+ projected[i]._state_db_row_id = identity.id;
1717
+ if (identity.id !== row.id)
1718
+ projected[i]._state_db_source_key = identity.sourceKey;
1719
+ if (identity.changed)
1720
+ projected[i]._state_db_authored_change = true;
1721
+ }
1722
+ }
1723
+ }
1724
+ // The kept reply and summary have independent archive identities; neither can consume the other's copy.
1725
+ const withCarrierSummary = (row, message) => {
1726
+ const carrier = compactionCarrier(row);
1727
+ return carrier ? [message, { role: 'assistant', content: carrier.summary, timestamp: row.timestamp,
1728
+ _context_compression_marker: true, ...(hasId ? { _state_db_row_id: identities?.get(Number(row.id))?.summaryId ?? row.id } : {}) }] : [message];
1729
+ };
1730
+ projected = rows.flatMap((row, i) => withCarrierSummary(row, projected[i]));
824
1731
  // TAL-493: the database file's identity, so a marker taken in a replaced state.db is never applied to its successor.
1732
+ const generation = stateDbGeneration(dbPath);
825
1733
  if (hasId) {
826
- const generation = stateDbGeneration(dbPath);
827
1734
  for (const m of projected)
828
1735
  m._state_db_generation = generation;
829
1736
  }
830
- return { rows: projected, idCapable: hasId, ok: true };
1737
+ let anchor;
1738
+ if (opts.afterId !== undefined) {
1739
+ anchor = projected.find((m) => m._state_db_row_id === opts.afterId);
1740
+ if (!anchor) {
1741
+ const row = db.prepare(`SELECT ${selected.join(', ')}, session_id FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')}) AND id = ?${activeClause} LIMIT 1`).get(...chain, opts.afterId);
1742
+ if (row)
1743
+ anchor = { ...withCarrierSummary(row, projectStateDbMessage(row, true)).at(-1), _state_db_generation: generation };
1744
+ }
1745
+ }
1746
+ let requiresFullRead = false;
1747
+ if (opts.afterId !== undefined && opts.unidentified) {
1748
+ const matches = projected.filter((m) => opts.unidentified.some((keys) => keys.has(stateDbMessageKey(m))));
1749
+ if (matches.length) {
1750
+ // Projection can normalize content/timestamps. A bounded role-key superset is conservative copy detection.
1751
+ const roles = [...new Set(matches.map((m) => m.role))];
1752
+ const covered = db.prepare(`SELECT 1 FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')}) AND id <= ? AND role IN (SELECT value FROM json_each(?))${activeClause} LIMIT 1`).get(...chain, opts.afterId, JSON.stringify(roles));
1753
+ requiresFullRead = Boolean(covered) || available.has('message_uid');
1754
+ }
1755
+ }
1756
+ return { rows: projected, idCapable: hasId, ok: true, reason: null, generation, requiresFullRead, ...(anchor ? { anchor } : {}) };
831
1757
  }
832
- catch {
833
- return none;
1758
+ catch (error) {
1759
+ return { ...none, reason: stateDbReadFailure(error).reason };
834
1760
  }
835
1761
  finally {
836
- db.close();
1762
+ finishStateDbRead(db);
837
1763
  }
838
1764
  }
839
1765
  /**
840
1766
  * TAL-529: `sid` and each compression continuation the Agent rotated it to, down to the live tip (Python
841
- * `resolve_live_compression_tip`). The walk stops at the last unambiguous row: no continuation, several, a cycle, or 20
842
- * hops. A missing or unreadable database is `[sid]`.
1767
+ * `resolve_live_compression_tip`). The walk stops at the last unambiguous row: no continuation, several, a cycle, or
1768
+ * `COMPRESSION_CHAIN_CAP` hops. A missing or unreadable database is `[sid]`.
843
1769
  */
844
1770
  export function stateDbCompressionLineage(dbPath, sid) {
845
1771
  const lineage = [sid.trim()];
@@ -853,14 +1779,18 @@ export function stateDbCompressionLineage(dbPath, sid) {
853
1779
  return lineage;
854
1780
  }
855
1781
  try {
1782
+ const local = localLineages(db, lineage[0]).get(lineage[0]);
1783
+ if (local)
1784
+ return [...local.ids];
856
1785
  const sessionCols = tableColumns(db, 'sessions');
857
1786
  if (!['id', 'parent_session_id', 'end_reason', 'ended_at', 'started_at', 'source'].every((c) => sessionCols.has(c)))
858
1787
  return lineage;
859
1788
  const sessionSource = sessionCols.has('session_source') ? 'session_source' : 'NULL AS session_source';
860
- const cols = `id, source, ${sessionSource}, started_at, parent_session_id, ended_at, end_reason`;
1789
+ const modelConfig = sessionCols.has('model_config') ? 'model_config' : 'NULL AS model_config';
1790
+ const cols = `id, source, ${sessionSource}, started_at, parent_session_id, ended_at, end_reason, ${modelConfig}`;
861
1791
  let current = db.prepare(`SELECT ${cols} FROM sessions WHERE id = ?`).get(lineage[0]);
862
1792
  const children = db.prepare(`SELECT ${cols} FROM sessions WHERE parent_session_id = ?`);
863
- for (let hop = 0; hop < 20 && current; hop += 1) {
1793
+ for (let hop = 0; hop < COMPRESSION_CHAIN_CAP && current; hop += 1) {
864
1794
  const parent = current;
865
1795
  const next = children.all(str(parent.id)).filter((child) => isContinuationSession(parent, child));
866
1796
  if (next.length !== 1 || lineage.includes(str(next[0].id)))
@@ -874,7 +1804,7 @@ export function stateDbCompressionLineage(dbPath, sid) {
874
1804
  return lineage.slice(0, 1);
875
1805
  }
876
1806
  finally {
877
- db.close();
1807
+ finishStateDbRead(db);
878
1808
  }
879
1809
  }
880
1810
  function lineageReportRow(row, role) {
@@ -884,7 +1814,7 @@ function lineageReportRow(row, role) {
884
1814
  end_reason: row.end_reason ?? null, active: ended === null, archived: false,
885
1815
  };
886
1816
  }
887
- const emptyLineageReport = (sid) => ({
1817
+ export const emptyLineageReport = (sid) => ({
888
1818
  mutation: false, found: false, session_id: sid, lineage_key: sid, tip_session_id: sid, total_segments: 0, materialized_segments: 0, segments: [], children: [], manual_review: false,
889
1819
  });
890
1820
  /**
@@ -893,7 +1823,7 @@ const emptyLineageReport = (sid) => ({
893
1823
  * newest first. A cycle, a hop limit hit, or a continuation off the walked path marks `manual_review`. A missing,
894
1824
  * unreadable, or too-old database, or an unknown id, answers `found: false`.
895
1825
  */
896
- export function stateDbLineageReport(dbPath, rawSid, maxHops = 20) {
1826
+ export function stateDbLineageReport(dbPath, rawSid, maxHops = COMPRESSION_CHAIN_CAP) {
897
1827
  const sid = rawSid.trim();
898
1828
  if (!sid || !existsSync(dbPath))
899
1829
  return emptyLineageReport(sid);
@@ -905,6 +1835,7 @@ export function stateDbLineageReport(dbPath, rawSid, maxHops = 20) {
905
1835
  return emptyLineageReport(sid);
906
1836
  }
907
1837
  try {
1838
+ const local = localLineages(db, sid).get(sid);
908
1839
  const cols = tableColumns(db, 'sessions');
909
1840
  if (!['id', 'parent_session_id', 'end_reason'].every((c) => cols.has(c)))
910
1841
  return emptyLineageReport(sid);
@@ -915,10 +1846,10 @@ export function stateDbLineageReport(dbPath, rawSid, maxHops = 20) {
915
1846
  const target = fetch(sid);
916
1847
  if (!target)
917
1848
  return emptyLineageReport(sid);
918
- const segments = [target];
919
- const seen = new Set([sid]);
920
- let manualReview = true;
921
- for (let hop = 0; hop < maxHops; hop += 1) {
1849
+ const segments = local ? [...local.rows].reverse().slice(0, Math.max(1, maxHops)) : [target];
1850
+ const seen = new Set(segments.map((row) => str(row.id)));
1851
+ let manualReview = local ? local.ids.length > maxHops : true;
1852
+ for (let hop = 0; !local && hop < maxHops; hop += 1) {
922
1853
  const current = segments[segments.length - 1];
923
1854
  const parentId = str(current.parent_session_id);
924
1855
  const parent = fetch(parentId);
@@ -959,7 +1890,7 @@ export function stateDbLineageReport(dbPath, rawSid, maxHops = 20) {
959
1890
  return emptyLineageReport(sid);
960
1891
  }
961
1892
  finally {
962
- db.close();
1893
+ finishStateDbRead(db);
963
1894
  }
964
1895
  }
965
1896
  /** The `sessions` row for `sid` (source and lifecycle columns), or null. */
@@ -985,7 +1916,7 @@ export function stateDbSessionRow(dbPath, sid) {
985
1916
  return null;
986
1917
  }
987
1918
  finally {
988
- db.close();
1919
+ finishStateDbRead(db);
989
1920
  }
990
1921
  }
991
1922
  /**
@@ -1011,21 +1942,31 @@ export function insightsSessionRows(dbPath, cutoff) {
1011
1942
  const col = (name) => (cols.has(name) ? `s.${name}` : 'NULL');
1012
1943
  const messageCols = tableColumns(db, 'messages');
1013
1944
  const hasMessages = messageCols.has('session_id') && messageCols.has('timestamp');
1014
- let indexed = false;
1015
- try {
1016
- indexed = hasMessages && db.prepare('PRAGMA index_list(messages)').all().some((r) => r.name === 'idx_messages_session');
1017
- }
1018
- catch {
1019
- indexed = false;
1020
- }
1945
+ const indexed = hasMessages && tableIndexes(db, 'messages').has('idx_messages_session');
1021
1946
  // Without the index a per-session lookup scans messages once per session; one grouped scan replaces it.
1022
- const lastMessage = !hasMessages ? 'NULL' : indexed ? '(SELECT MAX(m.timestamp) FROM messages m WHERE m.session_id = s.id)' : 'lm.last_message_at';
1023
- const join = hasMessages && !indexed ? 'LEFT JOIN (SELECT session_id, MAX(timestamp) AS last_message_at FROM messages GROUP BY session_id) lm ON lm.session_id = s.id' : '';
1947
+ const lastMessage = !hasMessages ? 'NULL' : indexed ? lastMessageTimestamp(indexed) : 'lm.last_message_at';
1948
+ const messageCte = hasMessages && !indexed ? 'latest_messages AS MATERIALIZED (SELECT session_id, MAX(timestamp) AS last_message_at FROM messages GROUP BY session_id)' : '';
1949
+ const join = messageCte ? 'LEFT JOIN latest_messages lm ON lm.session_id = s.id' : '';
1024
1950
  const source = cols.has('source') ? "LOWER(COALESCE(s.source, '')) != 'webui'" : '1';
1025
- const rows = db.prepare(`SELECT * FROM (SELECT s.id AS id, ${col('model')} AS model, ${col('message_count')} AS message_count, ${col('input_tokens')} AS input_tokens,
1951
+ const activityIndexed = cols.has('last_activity_at') && tableIndexes(db, 'sessions').has('idx_sessions_effective_activity');
1952
+ const activity = activityIndexed ? freshestActivity('s.last_activity_at', `MAX(COALESCE(${col('ended_at')}, 0), COALESCE(${lastMessage}, 0))`)
1953
+ : `MAX(COALESCE(s.started_at, 0), COALESCE(${col('ended_at')}, 0), COALESCE(${lastMessage}, 0))`;
1954
+ // The heartbeat can lag a message or end time; union their candidates before the exact activity check.
1955
+ const candidates = activityIndexed ? [
1956
+ 'SELECT s.id FROM sessions s INDEXED BY idx_sessions_effective_activity WHERE COALESCE(s.last_activity_at, s.started_at) >= ?',
1957
+ 'SELECT id FROM sessions WHERE started_at >= ?',
1958
+ ...(cols.has('ended_at') ? ['SELECT id FROM sessions WHERE ended_at >= ?'] : []),
1959
+ ...(hasMessages ? [indexed ? 'SELECT DISTINCT session_id AS id FROM messages WHERE timestamp >= ?' : 'SELECT session_id AS id FROM latest_messages WHERE last_message_at >= ?'] : []),
1960
+ ] : [];
1961
+ const ctes = [messageCte, ...(activityIndexed ? [`candidates AS MATERIALIZED (SELECT DISTINCT id FROM (${candidates.join(' UNION ALL ')}))`] : [])].filter(Boolean);
1962
+ const withClause = ctes.length ? `WITH ${ctes.join(', ')} ` : '';
1963
+ // CROSS JOIN keeps the candidate IDs ahead of the expensive per-session freshness projection.
1964
+ const from = activityIndexed ? 'candidates c CROSS JOIN sessions s ON s.id = c.id' : 'sessions s';
1965
+ const ordering = activityIndexed ? ' ORDER BY COALESCE(s.last_activity_at, s.started_at) DESC' : '';
1966
+ const rows = db.prepare(`${withClause}SELECT * FROM (SELECT s.id AS id, ${col('model')} AS model, ${col('message_count')} AS message_count, ${col('input_tokens')} AS input_tokens,
1026
1967
  ${col('output_tokens')} AS output_tokens, ${col('cache_read_tokens')} AS cache_read_tokens, ${col('estimated_cost_usd')} AS estimated_cost_usd, s.started_at AS started_at,
1027
- MAX(COALESCE(s.started_at, 0), COALESCE(${col('ended_at')}, 0), COALESCE(${lastMessage}, 0)) AS last_activity
1028
- FROM sessions s ${join} WHERE ${source}) WHERE last_activity >= ?`).all(cutoff);
1968
+ ${activity} AS last_activity
1969
+ FROM ${from} ${join} WHERE ${source}${ordering}) WHERE last_activity >= ?`).all(...Array(candidates.length + 1).fill(cutoff));
1029
1970
  return rows.map((r) => ({
1030
1971
  session_id: str(r.id), model: r.model, message_count: r.message_count, input_tokens: r.input_tokens, output_tokens: r.output_tokens,
1031
1972
  cache_read_tokens: r.cache_read_tokens, estimated_cost: r.estimated_cost_usd, created_at: r.started_at, updated_at: r.last_activity,
@@ -1035,7 +1976,7 @@ export function insightsSessionRows(dbPath, cutoff) {
1035
1976
  return [];
1036
1977
  }
1037
1978
  finally {
1038
- db.close();
1979
+ finishStateDbRead(db);
1039
1980
  }
1040
1981
  }
1041
1982
  /** A state.db `timestamp` in unix seconds: a number, or an ISO-8601 string (Python `fromisoformat`); null when unreadable. */
@@ -1064,7 +2005,7 @@ export function countConversationRounds(dbPath, sid, since = null) {
1064
2005
  rows = db.prepare('SELECT role, timestamp FROM messages WHERE session_id = ? ORDER BY timestamp ASC').all(sid);
1065
2006
  }
1066
2007
  finally {
1067
- db.close();
2008
+ finishStateDbRead(db);
1068
2009
  }
1069
2010
  }
1070
2011
  catch {
@@ -1105,7 +2046,7 @@ export function stateDbTailToolContent(dbPath, sid) {
1105
2046
  return typeof content === 'string' ? content : null;
1106
2047
  }
1107
2048
  finally {
1108
- db.close();
2049
+ finishStateDbRead(db);
1109
2050
  }
1110
2051
  }
1111
2052
  catch {