@maudecode/talaria-web 1.16.1 → 1.18.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 (445) hide show
  1. package/_release.json +8 -8
  2. package/dist/api/auth-raw.d.ts +1 -1
  3. package/dist/api/auth-raw.js +4 -4
  4. package/dist/api/auth-raw.js.map +1 -1
  5. package/dist/api/automation-raw.d.ts +6 -0
  6. package/dist/api/automation-raw.js +59 -9
  7. package/dist/api/automation-raw.js.map +1 -1
  8. package/dist/api/automation-router.d.ts +163 -10
  9. package/dist/api/automation-router.js +25 -8
  10. package/dist/api/automation-router.js.map +1 -1
  11. package/dist/api/chat-router.d.ts +700 -15
  12. package/dist/api/chat-router.js +74 -55
  13. package/dist/api/chat-router.js.map +1 -1
  14. package/dist/api/git-router.d.ts +47 -2
  15. package/dist/api/git-router.js +31 -22
  16. package/dist/api/git-router.js.map +1 -1
  17. package/dist/api/raw-routes.d.ts +14 -0
  18. package/dist/api/raw-routes.js +277 -36
  19. package/dist/api/raw-routes.js.map +1 -1
  20. package/dist/api/router.d.ts +10 -0
  21. package/dist/api/router.js +10 -8
  22. package/dist/api/router.js.map +1 -1
  23. package/dist/api/sessions-router.d.ts +1710 -73
  24. package/dist/api/sessions-router.js +164 -10
  25. package/dist/api/sessions-router.js.map +1 -1
  26. package/dist/api/settings-router.d.ts +132 -2
  27. package/dist/api/settings-router.js +15 -5
  28. package/dist/api/settings-router.js.map +1 -1
  29. package/dist/api/sse-routes.d.ts +16 -3
  30. package/dist/api/sse-routes.js +194 -67
  31. package/dist/api/sse-routes.js.map +1 -1
  32. package/dist/api/tools-raw.js +12 -25
  33. package/dist/api/tools-raw.js.map +1 -1
  34. package/dist/api/tools-router.d.ts +425 -19
  35. package/dist/api/tools-router.js +157 -26
  36. package/dist/api/tools-router.js.map +1 -1
  37. package/dist/app.d.ts +0 -9377
  38. package/dist/app.js +42 -17
  39. package/dist/app.js.map +1 -1
  40. package/dist/auth/gate.js +8 -2
  41. package/dist/auth/gate.js.map +1 -1
  42. package/dist/auth/oidc.d.ts +5 -2
  43. package/dist/auth/oidc.js +8 -7
  44. package/dist/auth/oidc.js.map +1 -1
  45. package/dist/bin/talaria-web.js +1 -1
  46. package/dist/bin/talaria-web.js.map +1 -1
  47. package/dist/cli/dotenv.js +1 -0
  48. package/dist/cli/dotenv.js.map +1 -1
  49. package/dist/config/agent-config.d.ts +2 -0
  50. package/dist/config/agent-config.js +12 -2
  51. package/dist/config/agent-config.js.map +1 -1
  52. package/dist/config.d.ts +2 -0
  53. package/dist/config.js +5 -1
  54. package/dist/config.js.map +1 -1
  55. package/dist/http/context.d.ts +20 -3
  56. package/dist/http/context.js +41 -8
  57. package/dist/http/context.js.map +1 -1
  58. package/dist/http/csp.d.ts +2 -1
  59. package/dist/http/csp.js +3 -2
  60. package/dist/http/csp.js.map +1 -1
  61. package/dist/onboarding.js +4 -0
  62. package/dist/onboarding.js.map +1 -1
  63. package/dist/profiles/profiles.d.ts +10 -0
  64. package/dist/profiles/profiles.js +40 -2
  65. package/dist/profiles/profiles.js.map +1 -1
  66. package/dist/providers/catalog.d.ts +101 -8
  67. package/dist/providers/catalog.js +463 -123
  68. package/dist/providers/catalog.js.map +1 -1
  69. package/dist/raster-image.d.ts +7 -0
  70. package/dist/raster-image.js +267 -0
  71. package/dist/raster-image.js.map +1 -0
  72. package/dist/redact.d.ts +1 -0
  73. package/dist/redact.js +354 -44
  74. package/dist/redact.js.map +1 -1
  75. package/dist/runtime.js +111 -27
  76. package/dist/runtime.js.map +1 -1
  77. package/dist/server.js +5 -0
  78. package/dist/server.js.map +1 -1
  79. package/dist/sessions/anchor.d.ts +2 -0
  80. package/dist/sessions/anchor.js +8 -3
  81. package/dist/sessions/anchor.js.map +1 -1
  82. package/dist/sessions/background-tasks.d.ts +2 -9
  83. package/dist/sessions/background-tasks.js +2 -12
  84. package/dist/sessions/background-tasks.js.map +1 -1
  85. package/dist/sessions/claude-code.d.ts +19 -0
  86. package/dist/sessions/claude-code.js +250 -0
  87. package/dist/sessions/claude-code.js.map +1 -0
  88. package/dist/sessions/cli-sessions.d.ts +15 -4
  89. package/dist/sessions/cli-sessions.js +120 -29
  90. package/dist/sessions/cli-sessions.js.map +1 -1
  91. package/dist/sessions/completions.d.ts +61 -9
  92. package/dist/sessions/completions.js +282 -34
  93. package/dist/sessions/completions.js.map +1 -1
  94. package/dist/sessions/compress.d.ts +14 -1
  95. package/dist/sessions/compress.js +52 -11
  96. package/dist/sessions/compress.js.map +1 -1
  97. package/dist/sessions/export.d.ts +5 -0
  98. package/dist/sessions/export.js +30 -0
  99. package/dist/sessions/export.js.map +1 -1
  100. package/dist/sessions/gateway-watcher.d.ts +7 -3
  101. package/dist/sessions/gateway-watcher.js +31 -19
  102. package/dist/sessions/gateway-watcher.js.map +1 -1
  103. package/dist/sessions/handoff.d.ts +32 -0
  104. package/dist/sessions/handoff.js +114 -0
  105. package/dist/sessions/handoff.js.map +1 -0
  106. package/dist/sessions/journal.d.ts +8 -0
  107. package/dist/sessions/journal.js +44 -10
  108. package/dist/sessions/journal.js.map +1 -1
  109. package/dist/sessions/list.d.ts +13 -3
  110. package/dist/sessions/list.js +46 -15
  111. package/dist/sessions/list.js.map +1 -1
  112. package/dist/sessions/merge.d.ts +53 -5
  113. package/dist/sessions/merge.js +219 -23
  114. package/dist/sessions/merge.js.map +1 -1
  115. package/dist/sessions/recovery.d.ts +22 -0
  116. package/dist/sessions/recovery.js +506 -0
  117. package/dist/sessions/recovery.js.map +1 -0
  118. package/dist/sessions/relay.d.ts +21 -2
  119. package/dist/sessions/relay.js +156 -11
  120. package/dist/sessions/relay.js.map +1 -1
  121. package/dist/sessions/service.d.ts +152 -8
  122. package/dist/sessions/service.js +640 -72
  123. package/dist/sessions/service.js.map +1 -1
  124. package/dist/sessions/session.d.ts +17 -1
  125. package/dist/sessions/session.js +63 -4
  126. package/dist/sessions/session.js.map +1 -1
  127. package/dist/sessions/state-db-worker.d.ts +1 -0
  128. package/dist/sessions/state-db-worker.js +33 -0
  129. package/dist/sessions/state-db-worker.js.map +1 -0
  130. package/dist/sessions/state-db.d.ts +87 -4
  131. package/dist/sessions/state-db.js +417 -8
  132. package/dist/sessions/state-db.js.map +1 -1
  133. package/dist/sessions/store.d.ts +7 -0
  134. package/dist/sessions/store.js +36 -22
  135. package/dist/sessions/store.js.map +1 -1
  136. package/dist/sessions/streams.d.ts +10 -0
  137. package/dist/sessions/streams.js +14 -3
  138. package/dist/sessions/streams.js.map +1 -1
  139. package/dist/sessions/titles.d.ts +2 -0
  140. package/dist/sessions/titles.js +4 -0
  141. package/dist/sessions/titles.js.map +1 -1
  142. package/dist/sessions/todo.d.ts +6 -1
  143. package/dist/sessions/todo.js +11 -4
  144. package/dist/sessions/todo.js.map +1 -1
  145. package/dist/sessions/tool-display.d.ts +12 -1
  146. package/dist/sessions/tool-display.js +50 -1
  147. package/dist/sessions/tool-display.js.map +1 -1
  148. package/dist/sessions/turn-context.d.ts +1 -1
  149. package/dist/sessions/turn-context.js +11 -7
  150. package/dist/sessions/turn-context.js.map +1 -1
  151. package/dist/sessions/turn.d.ts +67 -11
  152. package/dist/sessions/turn.js +413 -111
  153. package/dist/sessions/turn.js.map +1 -1
  154. package/dist/sessions/wakeup-pause.d.ts +59 -0
  155. package/dist/sessions/wakeup-pause.js +181 -0
  156. package/dist/sessions/wakeup-pause.js.map +1 -0
  157. package/dist/settings.d.ts +20 -2
  158. package/dist/settings.js +45 -9
  159. package/dist/settings.js.map +1 -1
  160. package/dist/sidecar/fake.d.ts +4 -4
  161. package/dist/sidecar/fake.js +5 -2
  162. package/dist/sidecar/fake.js.map +1 -1
  163. package/dist/spa.js +2 -1
  164. package/dist/spa.js.map +1 -1
  165. package/dist/startup.d.ts +13 -5
  166. package/dist/startup.js +48 -0
  167. package/dist/startup.js.map +1 -1
  168. package/dist/test/harness.js +2 -0
  169. package/dist/test/harness.js.map +1 -1
  170. package/dist/test/ts-worker-hooks.d.ts +1 -0
  171. package/dist/test/ts-worker-hooks.js +24 -0
  172. package/dist/test/ts-worker-hooks.js.map +1 -0
  173. package/dist/text/diff.d.ts +9 -14
  174. package/dist/text/diff.js +74 -108
  175. package/dist/text/diff.js.map +1 -1
  176. package/dist/tools/crons.js +45 -8
  177. package/dist/tools/crons.js.map +1 -1
  178. package/dist/tools/dashboard-plugins.d.ts +53 -0
  179. package/dist/tools/dashboard-plugins.js +188 -0
  180. package/dist/tools/dashboard-plugins.js.map +1 -0
  181. package/dist/tools/extensions.d.ts +13 -1
  182. package/dist/tools/extensions.js +132 -60
  183. package/dist/tools/extensions.js.map +1 -1
  184. package/dist/tools/health.d.ts +32 -0
  185. package/dist/tools/health.js +111 -19
  186. package/dist/tools/health.js.map +1 -1
  187. package/dist/tools/insights.d.ts +8 -2
  188. package/dist/tools/insights.js +18 -2
  189. package/dist/tools/insights.js.map +1 -1
  190. package/dist/tools/kanban.d.ts +41 -1
  191. package/dist/tools/kanban.js +113 -2
  192. package/dist/tools/kanban.js.map +1 -1
  193. package/dist/tools/mcp.d.ts +15 -1
  194. package/dist/tools/mcp.js +99 -5
  195. package/dist/tools/mcp.js.map +1 -1
  196. package/dist/tools/project-os.d.ts +11 -0
  197. package/dist/tools/project-os.js +267 -0
  198. package/dist/tools/project-os.js.map +1 -0
  199. package/dist/tools/skills.d.ts +5 -1
  200. package/dist/tools/skills.js +5 -3
  201. package/dist/tools/skills.js.map +1 -1
  202. package/dist/tools/terminal.d.ts +1 -0
  203. package/dist/tools/terminal.js +1 -1
  204. package/dist/tools/terminal.js.map +1 -1
  205. package/dist/tools/updates.d.ts +33 -8
  206. package/dist/tools/updates.js +236 -57
  207. package/dist/tools/updates.js.map +1 -1
  208. package/dist/tools/wiki.d.ts +68 -0
  209. package/dist/tools/wiki.js +413 -0
  210. package/dist/tools/wiki.js.map +1 -0
  211. package/dist/workspace/escape.d.ts +61 -0
  212. package/dist/workspace/escape.js +149 -0
  213. package/dist/workspace/escape.js.map +1 -0
  214. package/dist/workspace/extract.d.ts +22 -0
  215. package/dist/workspace/extract.js +308 -0
  216. package/dist/workspace/extract.js.map +1 -0
  217. package/dist/workspace/fs.d.ts +19 -3
  218. package/dist/workspace/fs.js +96 -23
  219. package/dist/workspace/fs.js.map +1 -1
  220. package/dist/workspace/git.d.ts +18 -0
  221. package/dist/workspace/git.js +38 -25
  222. package/dist/workspace/git.js.map +1 -1
  223. package/dist/workspace/media.d.ts +4 -4
  224. package/dist/workspace/media.js +18 -45
  225. package/dist/workspace/media.js.map +1 -1
  226. package/dist/workspace/preview.d.ts +28 -0
  227. package/dist/workspace/preview.js +110 -0
  228. package/dist/workspace/preview.js.map +1 -0
  229. package/dist/workspace/rollback.d.ts +36 -21
  230. package/dist/workspace/rollback.js +179 -270
  231. package/dist/workspace/rollback.js.map +1 -1
  232. package/dist/workspace/unzip.js +4 -1
  233. package/dist/workspace/unzip.js.map +1 -1
  234. package/dist/workspace/upload.d.ts +9 -4
  235. package/dist/workspace/upload.js +9 -3
  236. package/dist/workspace/upload.js.map +1 -1
  237. package/dist/workspace/worktrees.d.ts +2 -1
  238. package/dist/workspace/worktrees.js +14 -15
  239. package/dist/workspace/worktrees.js.map +1 -1
  240. package/package.json +4 -2
  241. package/sidecar/agent_dependency.json +4 -4
  242. package/sidecar/scripts/replay_sidecar.py +25 -1
  243. package/sidecar/talaria_sidecar/__init__.py +1 -1
  244. package/sidecar/talaria_sidecar/__main__.py +7 -1
  245. package/sidecar/talaria_sidecar/home.py +52 -2
  246. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  247. package/sidecar/talaria_sidecar/methods/aux.py +39 -5
  248. package/sidecar/talaria_sidecar/methods/chat.py +431 -43
  249. package/sidecar/talaria_sidecar/methods/checkpoints.py +89 -0
  250. package/sidecar/talaria_sidecar/methods/commands.py +96 -2
  251. package/sidecar/talaria_sidecar/methods/cron.py +39 -8
  252. package/sidecar/talaria_sidecar/methods/gateway.py +32 -15
  253. package/sidecar/talaria_sidecar/methods/kanban.py +140 -19
  254. package/sidecar/talaria_sidecar/methods/mcp.py +1 -1
  255. package/sidecar/talaria_sidecar/methods/process.py +115 -2
  256. package/sidecar/talaria_sidecar/methods/profiles.py +84 -8
  257. package/sidecar/talaria_sidecar/methods/providers.py +27 -8
  258. package/sidecar/talaria_sidecar/methods/runtime.py +3 -0
  259. package/sidecar/talaria_sidecar/methods/state_db.py +39 -5
  260. package/sidecar/talaria_sidecar/methods/text.py +17 -1
  261. package/sidecar/talaria_sidecar/methods/usage.py +257 -4
  262. package/sidecar/talaria_sidecar/runtime.py +17 -1
  263. package/static/dist/FILES.txt +127 -125
  264. package/static/dist/assets/AppShell-g3bkgnvt.js +1 -0
  265. package/static/dist/assets/ChatPage-oo7bdan6.js +25 -0
  266. package/static/dist/assets/Dialog-g355qxhp.js +1 -0
  267. package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +1 -0
  268. package/static/dist/assets/HubRoute-lmkrbw47.js +2 -0
  269. package/static/dist/assets/{Markdown-jqrx5es0.js → Markdown-j4kut4j2.js} +125 -125
  270. package/static/dist/assets/RightPanel-ohlsdzfl.js +1 -0
  271. package/static/dist/assets/Select-kddq75qc.js +1 -0
  272. package/static/dist/assets/SessionListPanel-lu3oy0hw.js +1 -0
  273. package/static/dist/assets/SettingsLayout-o9bv4mgb.js +1 -0
  274. package/static/dist/assets/States-eq6fe29m.js +1 -0
  275. package/static/dist/assets/Toaster-pc247u9f.js +1 -0
  276. package/static/dist/assets/_app-f8n1i0m1.js +1 -0
  277. package/static/dist/assets/_app.ext._extensionId-gfbzpoou.js +1 -0
  278. package/static/dist/assets/_app.index-bzzv80hk.js +1 -0
  279. package/static/dist/assets/_app.insights-dzixbryi.js +1 -0
  280. package/static/dist/assets/_app.kanban-c6hep2us.js +1 -0
  281. package/static/dist/assets/_app.logs-i69qds98.js +1 -0
  282. package/static/dist/assets/_app.memory-kl4vf0gd.js +1 -0
  283. package/static/dist/assets/_app.profiles-b8zinmyp.js +1 -0
  284. package/static/dist/assets/{_app.session._sessionId-hh27wwu2.js → _app.session._sessionId-fn1bhde6.js} +1 -1
  285. package/static/dist/assets/_app.settings-cg8plwmf.js +1 -0
  286. package/static/dist/assets/_app.settings._section-bkwbi9zn.js +3 -0
  287. package/static/dist/assets/_app.skills-cs47sbvs.js +1 -0
  288. package/static/dist/assets/_app.tasks-dwvk64tc.js +1 -0
  289. package/static/dist/assets/_app.todos-c8xjm8p3.js +1 -0
  290. package/static/dist/assets/_app.workspaces-ehvd1gds.js +1 -0
  291. package/static/dist/assets/{abnfDiagram-VCTEODGH-oity9y4o.js → abnfDiagram-VCTEODGH-c8kdixke.js} +1 -1
  292. package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +1 -0
  293. package/static/dist/assets/{architectureDiagram-5GKGNRK7-ceaepnyr.js → architectureDiagram-5GKGNRK7-cku06x1p.js} +1 -1
  294. package/static/dist/assets/{back-eu64w89w.js → back-h7aun7ew.js} +1 -1
  295. package/static/dist/assets/{blockDiagram-I7D4REHJ-n79st4pj.js → blockDiagram-I7D4REHJ-jzthp1wa.js} +1 -1
  296. package/static/dist/assets/{c4Diagram-7LVT6UL2-mi73akzk.js → c4Diagram-7LVT6UL2-mwr67mt8.js} +1 -1
  297. package/static/dist/assets/{cancel-mzvkdjg8.js → cancel-fhi0vcnu.js} +1 -1
  298. package/static/dist/assets/channel-b2zo73z1.js +1 -0
  299. package/static/dist/assets/{chunk-4HAMMTFA-laat0g1q.js → chunk-4HAMMTFA-gpq1y228.js} +1 -1
  300. package/static/dist/assets/{chunk-75Z2AOVW-0psy1tr2.js → chunk-75Z2AOVW-ktje58c4.js} +1 -1
  301. package/static/dist/assets/{chunk-DU6HZSFF-lw8uox8x.js → chunk-DU6HZSFF-ofajdixh.js} +3 -3
  302. package/static/dist/assets/{chunk-F27PBJKO-erassib3.js → chunk-F27PBJKO-cmev85ti.js} +1 -1
  303. package/static/dist/assets/{chunk-GMAD6QVW-fz7rx619.js → chunk-GMAD6QVW-htjgt22y.js} +1 -1
  304. package/static/dist/assets/{chunk-GVQU2GXP-gdb0epb2.js → chunk-GVQU2GXP-qndvoxl7.js} +1 -1
  305. package/static/dist/assets/{chunk-IMKFNOWR-ea8t26qw.js → chunk-IMKFNOWR-e5bkd5q6.js} +1 -1
  306. package/static/dist/assets/{chunk-L3NEJ4N5-mb1gu9kz.js → chunk-L3NEJ4N5-mj1155r6.js} +1 -1
  307. package/static/dist/assets/{chunk-OSK3NFVY-eovmccvy.js → chunk-OSK3NFVY-e7gkikcw.js} +1 -1
  308. package/static/dist/assets/{chunk-P2QGCYS3-bo55zjo5.js → chunk-P2QGCYS3-f7t54xfm.js} +1 -1
  309. package/static/dist/assets/{chunk-POPQ4Y6H-ja7ohe4h.js → chunk-POPQ4Y6H-i927v9oa.js} +1 -1
  310. package/static/dist/assets/{chunk-PWAF6VOD-e30g9nus.js → chunk-PWAF6VOD-m84lmnyt.js} +1 -1
  311. package/static/dist/assets/{chunk-SHT3W25Y-gbb798f7.js → chunk-SHT3W25Y-mi0tlcu9.js} +1 -1
  312. package/static/dist/assets/{chunk-SVP7TREG-h2iw8urm.js → chunk-SVP7TREG-dnau702j.js} +1 -1
  313. package/static/dist/assets/{chunk-TICWLB2K-fd2cottz.js → chunk-TICWLB2K-edem1gkg.js} +1 -1
  314. package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +1 -0
  315. package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +1 -0
  316. package/static/dist/assets/{copied-h65fc3cx.js → copied-hcplv46p.js} +1 -1
  317. package/static/dist/assets/copy-magtofyp.js +1 -0
  318. package/static/dist/assets/{cynefin-OW5HDTMX-lbltyl5j.js → cynefin-OW5HDTMX-siqlulkl.js} +1 -1
  319. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-lxhsqq4a.js → cynefinDiagram-5FMLGOSQ-i648tah9.js} +1 -1
  320. package/static/dist/assets/{dagre-GXQ25YYZ-gn4nnaln.js → dagre-GXQ25YYZ-mwn6uc17.js} +1 -1
  321. package/static/dist/assets/{diagram-S7CK7UJ4-pa3ij6q7.js → diagram-S7CK7UJ4-l8abxaes.js} +1 -1
  322. package/static/dist/assets/{diagram-UQ7AKVKN-zbipyz57.js → diagram-UQ7AKVKN-mekq7zgq.js} +1 -1
  323. package/static/dist/assets/{diagram-VSXAHHWV-d1kbpzgf.js → diagram-VSXAHHWV-fnky6lvc.js} +1 -1
  324. package/static/dist/assets/{diagram-VX7I27RA-i3ty14xl.js → diagram-VX7I27RA-xe6r5qfw.js} +1 -1
  325. package/static/dist/assets/{diagram-Z3DM3KII-m8xefqa7.js → diagram-Z3DM3KII-nq07dg7k.js} +1 -1
  326. package/static/dist/assets/{ebnfDiagram-PWID7BFC-nz7wj05l.js → ebnfDiagram-PWID7BFC-c62ipm3g.js} +1 -1
  327. package/static/dist/assets/endpoints-klbxod4t.js +64 -0
  328. package/static/dist/assets/{erDiagram-RLTQ6QDP-fa41ib83.js → erDiagram-RLTQ6QDP-h39kmnzy.js} +1 -1
  329. package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +1 -0
  330. package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +1 -0
  331. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-n7zq01qu.js → ganttDiagram-EL5Y4UJY-ngbsyp37.js} +1 -1
  332. package/static/dist/assets/{gitGraph-4MIJSDKK-c89emb07.js → gitGraph-4MIJSDKK-fr2i7caa.js} +1 -1
  333. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-cz7cgr6w.js → gitGraphDiagram-WWUBYQGX-eravdti4.js} +1 -1
  334. package/static/dist/assets/{highlighted-body-KPVGNVTW-d9bapmni.js → highlighted-body-KPVGNVTW-jwpop9p8.js} +1 -1
  335. package/static/dist/assets/host-i828xuo7.js +1 -0
  336. package/static/dist/assets/index-eh8zilnv.css +2 -0
  337. package/static/dist/assets/index-r648f4o1.js +11 -0
  338. package/static/dist/assets/{info-A6RAGUB7-ndddhhhh.js → info-A6RAGUB7-h11x0dxo.js} +1 -1
  339. package/static/dist/assets/{infoDiagram-27XIBGKW-ccxigcpf.js → infoDiagram-27XIBGKW-dfh47dw9.js} +1 -1
  340. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-le0z9vtc.js → ishikawaDiagram-5VMMS53U-i6ypq351.js} +1 -1
  341. package/static/dist/assets/{journeyDiagram-3NMN7TZE-ivsmb26o.js → journeyDiagram-3NMN7TZE-lduf1z95.js} +1 -1
  342. package/static/dist/assets/{kanban-definition-UXKFOSKX-h6n2awt0.js → kanban-definition-UXKFOSKX-e18bu4pg.js} +1 -1
  343. package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +1 -0
  344. package/static/dist/assets/{line-dmzlfe0u.js → line-meizqsvn.js} +1 -1
  345. package/static/dist/assets/link-oot2yn73.js +1 -0
  346. package/static/dist/assets/login-gu28l9ou.js +1 -0
  347. package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +1 -0
  348. package/static/dist/assets/{mermaid-parser.core-gwyd5bnq.js → mermaid-parser.core-ntne45i9.js} +3 -3
  349. package/static/dist/assets/{mindmap-definition-YA3MSWOX-l68gg5h7.js → mindmap-definition-YA3MSWOX-27b26eok.js} +1 -1
  350. package/static/dist/assets/onboarding-lkphzu1s.js +1 -0
  351. package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +1 -0
  352. package/static/dist/assets/{packet-AYTQ26CC-gpv06065.js → packet-AYTQ26CC-l6jdka8p.js} +1 -1
  353. package/static/dist/assets/{pegDiagram-XKGWAZYB-e52vs23u.js → pegDiagram-XKGWAZYB-g1er3bxj.js} +1 -1
  354. package/static/dist/assets/{pie-WAS4IAKB-l6m7nz5s.js → pie-WAS4IAKB-i0ylcrt9.js} +1 -1
  355. package/static/dist/assets/{pieDiagram-E7YTZNPT-b4is36bq.js → pieDiagram-E7YTZNPT-fbz1e55z.js} +1 -1
  356. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-m8vt2788.js → quadrantDiagram-AXDQQJYC-hhue7d8o.js} +1 -1
  357. package/static/dist/assets/{radar-RG4KPBEZ-nxbrhiov.js → radar-RG4KPBEZ-f9b70aa4.js} +1 -1
  358. package/static/dist/assets/{railroad-74A4TZTK-222mu9hi.js → railroad-74A4TZTK-lgsuoeda.js} +1 -1
  359. package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +1 -0
  360. package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +1 -0
  361. package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +1 -0
  362. package/static/dist/assets/{railroadDiagram-O6MQD6OU-o8lldfzv.js → railroadDiagram-O6MQD6OU-eqpy9fnz.js} +1 -1
  363. package/static/dist/assets/refresh-abtu93ke.js +1 -0
  364. package/static/dist/assets/refresh-cw-hlwb02qv.js +1 -0
  365. package/static/dist/assets/{requirementDiagram-BXWQKSXE-ovyq38kv.js → requirementDiagram-BXWQKSXE-ogf6puhi.js} +1 -1
  366. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-ou7lznor.js → sankeyDiagram-P5KCCOFB-fqb8xcjf.js} +1 -1
  367. package/static/dist/assets/{saved-bazmcup9.js → saved-girnabkf.js} +1 -1
  368. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-ovckdzf2.js → sequenceDiagram-WJ2MYXX4-dlcz6qjz.js} +1 -1
  369. package/static/dist/assets/{settings_section_extensions_title-ertwwhlx.js → settings_section_extensions_title-h14cwk6f.js} +1 -1
  370. package/static/dist/assets/share._token-kl0r7ido.js +2 -0
  371. package/static/dist/assets/sliders-horizontal-n1zm34qq.js +1 -0
  372. package/static/dist/assets/sse-iwku0sjo.js +1 -0
  373. package/static/dist/assets/{stateDiagram-D77RDMKH-fcrexkkv.js → stateDiagram-D77RDMKH-fyoznpcs.js} +1 -1
  374. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +1 -0
  375. package/static/dist/assets/{swimlanes-42K2YHIH-f7usu8fw.js → swimlanes-42K2YHIH-oefldhfy.js} +1 -1
  376. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +8 -0
  377. package/static/dist/assets/{timeline-definition-24CTP7MA-gp1hvz9f.js → timeline-definition-24CTP7MA-b1vl2zil.js} +1 -1
  378. package/static/dist/assets/todoStore-jjr5yr78.js +1 -0
  379. package/static/dist/assets/{treeView-Q6P3EWNA-gb9gf11u.js → treeView-Q6P3EWNA-f7hcci5o.js} +1 -1
  380. package/static/dist/assets/{treemap-WGGIJYW6-nuvfnvvi.js → treemap-WGGIJYW6-nl85071a.js} +1 -1
  381. package/static/dist/assets/units-kgxzxfhi.js +1 -0
  382. package/static/dist/assets/{untitled-htelhc68.js → untitled-cikfjntn.js} +1 -1
  383. package/static/dist/assets/{useForm-h2t0u14e.js → useForm-kqoqg0k9.js} +1 -1
  384. package/static/dist/assets/useLocale-o59tbiea.js +1 -0
  385. package/static/dist/assets/{useParams-dqd64jv7.js → useParams-b4dq6n4q.js} +1 -1
  386. package/static/dist/assets/{useSearch-j30pvsy7.js → useSearch-ipfmv3he.js} +1 -1
  387. package/static/dist/assets/{vennDiagram-4TSXK5OY-fzchbzaj.js → vennDiagram-4TSXK5OY-h5hzbk0n.js} +1 -1
  388. package/static/dist/assets/{wardley-WFR3VGLG-m47mhfbh.js → wardley-WFR3VGLG-jftvpm3e.js} +1 -1
  389. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-odg6ct1z.js → wardleyDiagram-VM6X3IG4-hedh85ge.js} +1 -1
  390. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-mrhms5um.js → xychartDiagram-S5SC5T6Z-e49523ab.js} +1 -1
  391. package/static/dist/index.html +1 -1
  392. package/static/dist/sw.js +2 -2
  393. package/static/dist/assets/AppShell-ef3bhu32.js +0 -1
  394. package/static/dist/assets/Button-nx37f4u1.js +0 -1
  395. package/static/dist/assets/ChatPage-cdcpykcg.js +0 -25
  396. package/static/dist/assets/Dialog-mec42hvb.js +0 -1
  397. package/static/dist/assets/HubRoute-ecb65zpr.js +0 -2
  398. package/static/dist/assets/RightPanel-d1lgvk4s.js +0 -1
  399. package/static/dist/assets/Select-h4ivl9qf.js +0 -1
  400. package/static/dist/assets/SessionListPanel-ewib046b.js +0 -1
  401. package/static/dist/assets/SettingsLayout-lx654cup.js +0 -1
  402. package/static/dist/assets/States-ehsb6lzo.js +0 -1
  403. package/static/dist/assets/Toaster-mch1pa3t.js +0 -1
  404. package/static/dist/assets/_app-m8d961ef.js +0 -1
  405. package/static/dist/assets/_app.ext._extensionId-mp33hbzi.js +0 -1
  406. package/static/dist/assets/_app.index-dk6zp7e0.js +0 -1
  407. package/static/dist/assets/_app.insights-jt2fk4i1.js +0 -1
  408. package/static/dist/assets/_app.kanban-if4pubeu.js +0 -1
  409. package/static/dist/assets/_app.logs-mfaw9m6g.js +0 -1
  410. package/static/dist/assets/_app.memory-h3eaarb6.js +0 -1
  411. package/static/dist/assets/_app.profiles-ienktovs.js +0 -1
  412. package/static/dist/assets/_app.settings-dpoe0bzf.js +0 -1
  413. package/static/dist/assets/_app.settings._section-l468ynag.js +0 -3
  414. package/static/dist/assets/_app.skills-kcbncc8i.js +0 -1
  415. package/static/dist/assets/_app.tasks-occdrqhw.js +0 -2
  416. package/static/dist/assets/_app.todos-nnke2rcc.js +0 -1
  417. package/static/dist/assets/_app.workspaces-cdlatvwj.js +0 -1
  418. package/static/dist/assets/architecture-7GRP2DOG-hj11333z.js +0 -1
  419. package/static/dist/assets/channel-mbvmf2vv.js +0 -1
  420. package/static/dist/assets/classDiagram-ZZMXUADV-o8r05k0n.js +0 -1
  421. package/static/dist/assets/classDiagram-v2-VYDZK3BY-o8r05k0n.js +0 -1
  422. package/static/dist/assets/copy-e6977uwq.js +0 -1
  423. package/static/dist/assets/endpoints-k7yf25m0.js +0 -64
  424. package/static/dist/assets/eventmodeling-NTZA5JFV-kpbq09bx.js +0 -1
  425. package/static/dist/assets/flowDiagram-HODETNUW-lcz0kx14.js +0 -1
  426. package/static/dist/assets/index-h00usru2.js +0 -11
  427. package/static/dist/assets/index-n73ijkv2.css +0 -2
  428. package/static/dist/assets/lazyRouteComponent-m1rtyk4t.js +0 -1
  429. package/static/dist/assets/link-budeg5t9.js +0 -1
  430. package/static/dist/assets/login-kjhasvp8.js +0 -1
  431. package/static/dist/assets/matchContext-f8oy3lo7.js +0 -1
  432. package/static/dist/assets/mermaid-HWGCJPDP-jb5d211j.js +0 -1
  433. package/static/dist/assets/onboarding-cdfrk0pj.js +0 -1
  434. package/static/dist/assets/railroad-abnf-HS5TGJTU-kdvgl6x8.js +0 -1
  435. package/static/dist/assets/railroad-ebnf-LZEXJU2U-itiqslby.js +0 -1
  436. package/static/dist/assets/railroad-peg-WCYAUIDC-go7kov78.js +0 -1
  437. package/static/dist/assets/refresh-cw-kbcje1uy.js +0 -1
  438. package/static/dist/assets/search-jen74z4u.js +0 -1
  439. package/static/dist/assets/share._token-dan9hgvj.js +0 -2
  440. package/static/dist/assets/sliders-horizontal-f9cb3lh3.js +0 -1
  441. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-lcfraua2.js +0 -1
  442. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ogmanni0.js +0 -8
  443. package/static/dist/assets/todoStore-eqtid4r4.js +0 -1
  444. package/static/dist/assets/useLocale-hcsb84pj.js +0 -1
  445. package/static/dist/assets/useNavigate-nwy70ztj.js +0 -1
@@ -3,16 +3,20 @@ import { randomUUID } from 'node:crypto';
3
3
  import { rmSync } from 'node:fs';
4
4
  import { buildActiveTurnToken, copyJson, redactSessionData, redactValue, stripPublicInternalFields, withSceneToolDisplay } from '../redact.js';
5
5
  import { DraftVersionConflict, normalizeDraftVersion } from './drafts.js';
6
- import { allSessions, buildSessionListPayload, CLI_IDENTITY_FIELDS, isClaimableCliSource, isMessagingSessionRecord, withOwnerLocks, withSessionWireFlags, lineageRootId, mergeCliSidebarMetadata, sessionListResponse, sessionSearchMessageText, sessionSearchPreview } from './list.js';
6
+ import { allSessions, buildSessionListPayload, CLI_IDENTITY_FIELDS, isClaimableCliSource, isMessagingSessionRecord, ownerLockCandidates, withOwnerLocks, withSessionWireFlags, lineageRootId, mergeCliSidebarMetadata, sessionListResponse, sessionSearchMatches, sessionSearchMessageText, sessionSearchPreview, sessionSearchTerms } from './list.js';
7
7
  import { anchorSceneIntOrNull, fullToolResult, hydrateAnchorActivityScenes, normalizeAnchorSceneMessageRef, readAnchorSceneRows, storeAnchorScene, withTurnIds } from './anchor.js';
8
- import { isSafeSessionId, lastMessageTimestamp, Session, stripAttachedFilesMarker, titleFrom } from './session.js';
8
+ import { COMPRESSION_RECOVERY_ACTION_START_FOCUSED, compressionRecoveryPayload, isSafeSessionId, lastMessageTimestamp, Session, sharePath, stripAttachedFilesMarker, titleFrom } from './session.js';
9
9
  import { SessionBusy, SessionNotFound, statSignature } from './store.js';
10
- import { attachTodoState } from './todo.js';
11
- import { stateDbSessionMessages, stateDbSessionRow, stateDbSessionSources } from './state-db.js';
12
- import { anchorMessageKey, anchorSummary, CompressionJobs, visibleMessagesForAnchor } from './compress.js';
10
+ import { UNSETTLED_TODO_KEY, attachTodoState } from './todo.js';
11
+ import { isClaudeCodeSessionId } from './claude-code.js';
12
+ import { auditSessionRecovery, repairSafeSessionRecovery } from './recovery.js';
13
+ import { agentSessionRowsExisting, agentSessionRowsExistingAsync, CONVERSATION_ROUND_THRESHOLD, countConversationRounds, stateDbCompressionLineage, stateDbTailToolContent, stateDbLineageReport, stateDbSessionMessages, stateDbSessionRead, stateDbSessionRow, stateDbSessionSources, stateDbSessionSourcesAsync, stateDbTimestampSeconds } from './state-db.js';
14
+ import { completionIncomplete, fallbackHandoffSummary, HANDOFF_SYSTEM_PROMPT, handoffMarker, handoffPayload, handoffTranscript, messageHandoffPayload, sameHandoff } from './handoff.js';
15
+ import { anchorMessageKey, anchorSummary, CompressionJobs, compressionReference, visibleMessagesForAnchor } from './compress.js';
13
16
  import { SidecarError } from '../sidecar/client.js';
14
- import { attachmentObjects, isContextCompressionMarker, mergeSessionMessagesAppendOnly, pendingUserRow, sanitizeMessagesForApi, stripWorkspacePrefix, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withSceneRowMedia, withPendingUserTurn, withToolCallOutcomes, withoutRunningTurnOutput } from './merge.js';
17
+ import { agentSteerText, attachedFilesPrompt, attachmentObjects, checkpointTurnStart, dedupeContext, isContextCompressionMarker, journalOutputRows, looksLikeCurrentUserTurn, stoppedTurnContext, mergeSessionMessagesAppendOnly, messageIdentity, pendingUserRow, reasoningFieldsText, sanitizeMessagesForApi, stateDbSeenId, stripWorkspacePrefix, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withSceneRowMedia, withPendingUserTurn, withToolCallOutcomes, withoutRunningTurnOutput } from './merge.js';
15
18
  import { withBackgroundUpdates } from './background-updates.js';
19
+ import { sourceKind } from './source-kind.js';
16
20
  import { withBackgroundLinks } from './background-tasks.js';
17
21
  import { messagesForLimitedPayload, messageWindowForDisplay, MAX_MSG_LIMIT, parseMsgLimit, toolCallsForMessageWindow } from './window.js';
18
22
  import { redactText } from '../redact.js';
@@ -50,6 +54,9 @@ export async function ensureAgentRuntimeCurrent(sidecar) {
50
54
  function staleRuntimeFailure(error) {
51
55
  return new HttpFailure(409, error.message, { type: 'agent_runtime_stale', retryable: true, restart_scheduled: false, ...(error.data.agent_update_state !== undefined ? { agent_update_state: error.data.agent_update_state } : {}) });
52
56
  }
57
+ /** TAL-536: the most of a dead run's journal stale-stream cleanup reads (Python's recovery window). */
58
+ const RECOVERY_JOURNAL_MAX_BYTES = 8 * 1024 * 1024;
59
+ const RECOVERY_JOURNAL_MAX_ROWS = 65536;
53
60
  const isDict = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
54
61
  export class SessionService {
55
62
  deps;
@@ -60,6 +67,40 @@ export class SessionService {
60
67
  this.compressionJobs = new CompressionJobs();
61
68
  }
62
69
  get store() { return this.deps.store; }
70
+ /**
71
+ * TAL-577: user-turn admission shared by `chat.start` and server-side wakeups, after `ensureAgentRuntimeCurrent`.
72
+ * A requested workspace must be trusted; the session's own one, when its directory is gone, falls back to the last one.
73
+ */
74
+ turnWorkspace(s, requested) {
75
+ const ws = this.deps.workspaces;
76
+ if (requested !== undefined && requested !== null && requested !== '') {
77
+ try {
78
+ return ws.resolveTrusted(str(requested), s.profile);
79
+ }
80
+ catch (error) {
81
+ throw new HttpFailure(400, error.message);
82
+ }
83
+ }
84
+ try {
85
+ const [workspace, recovered] = ws.resolveImplicitWithRecovery(s.workspace, (p) => ws.lastWorkspace(p), s.profile);
86
+ if (recovered)
87
+ s.workspace = workspace;
88
+ return workspace;
89
+ }
90
+ catch (error) {
91
+ throw new HttpFailure(400, error.message);
92
+ }
93
+ }
94
+ /** A `@provider:` id is split; any other model an explicit pick did not just choose is checked against the catalog (TAL-542). */
95
+ turnModel(s, body = {}) {
96
+ const requestedModel = str(body.model) || s.model;
97
+ const requestedProvider = 'model_provider' in body ? body.model_provider : s.model_provider;
98
+ const [model, provider] = this.deps.modelStateFromRequest(requestedModel, requestedProvider, s.model_provider);
99
+ if (model !== requestedModel || !model || body.explicit_model_pick === true)
100
+ return [model, provider, model !== requestedModel];
101
+ const repaired = this.deps.repairSessionModel?.(s.profile ?? null, model, provider);
102
+ return repaired ? [repaired[0], repaired[1], true] : [model, provider, false];
103
+ }
63
104
  /**
64
105
  * TAL-186: rewrites a session's media references to their URLs. A local path is rewritten only when `/api/media`
65
106
  * serves it for this session, under the same resolution, so every rewritten reference loads.
@@ -146,6 +187,52 @@ export class SessionService {
146
187
  }
147
188
  /** TAL-358: batched `sessions.source` owners from the active profile's state.db. */
148
189
  stateDbSources = (ids) => stateDbSessionSources(join(this.deps.profileHome(this.deps.activeProfile()), 'state.db'), ids);
190
+ /**
191
+ * TAL-590 (Python #3238): the read-only CLI or API-server imports among `rows` whose active-profile state.db row is
192
+ * gone, so their source was deleted outside Web and no Web action could remove them. An unreadable state.db proves
193
+ * nothing (`agentSessionRowsExisting`); another profile's session or one with a run the runtime still holds is never
194
+ * an orphan, while a stream id a crash left behind does not count as one.
195
+ */
196
+ orphanedImports(rows) {
197
+ const candidates = this.orphanCandidates(rows);
198
+ if (!candidates.length)
199
+ return new Set();
200
+ const existing = agentSessionRowsExisting(this.stateDbPath(null), candidates);
201
+ return new Set(candidates.filter((sid) => !existing.has(sid)));
202
+ }
203
+ orphanCandidates(rows) {
204
+ return rows.filter((r) => r.read_only && ['cli', 'api'].includes(sourceKind(r)) && this.visibleToActiveProfile(str(r.profile) || null) && !this.activeRunBlocking(str(r.session_id)))
205
+ .map((r) => str(r.session_id));
206
+ }
207
+ isOrphanedImport(sid) {
208
+ try {
209
+ return this.orphanedImports([this.store.get(sid, { metadataOnly: true }).compact()]).size > 0;
210
+ }
211
+ catch {
212
+ return false;
213
+ }
214
+ }
215
+ /**
216
+ * The list's owner locks and orphan prune (an orphan's sidecar goes, as nothing else ever removes it), with their
217
+ * state.db reads made on the listing worker up front so the synchronous build never opens state.db (TAL-920).
218
+ */
219
+ async listStateDbLookups(rows) {
220
+ const dbPath = this.stateDbPath(null);
221
+ const ownerIds = ownerLockCandidates(rows);
222
+ const owners = ownerIds.length ? await stateDbSessionSourcesAsync(dbPath, ownerIds) : new Map();
223
+ const checked = new Set(this.orphanCandidates(rows));
224
+ const existing = checked.size ? await agentSessionRowsExistingAsync(dbPath, checked) : new Set();
225
+ return {
226
+ stateDbSources: () => owners,
227
+ pruneOrphanedImports: (current) => {
228
+ // Only an id the worker checked can be an orphan; a row that became a candidate meanwhile waits for the next list.
229
+ const orphans = new Set(this.orphanCandidates(current).filter((sid) => checked.has(sid) && !existing.has(sid)));
230
+ for (const sid of orphans)
231
+ this.store.deleteFiles(sid);
232
+ return orphans;
233
+ },
234
+ };
235
+ }
149
236
  /** The persisted flag folded with the state.db owner lock (TAL-358): the one read-only rule for mutation gates and the wire. */
150
237
  isReadOnly(s) {
151
238
  if (s.read_only)
@@ -174,6 +261,7 @@ export class SessionService {
174
261
  const synth = this.claimOrSynthesizeCliSession(sid);
175
262
  if (synth.session && synth.reason === 'materialized') {
176
263
  this.store.save(synth.session);
264
+ this.deps.titleClaimed?.(synth.session);
177
265
  return synth.session;
178
266
  }
179
267
  if (synth.session)
@@ -185,9 +273,18 @@ export class SessionService {
185
273
  }
186
274
  /** The Agent's state.db rows for this session (empty for subagent views, which never merge). */
187
275
  stateDbRows(s) {
276
+ return this.stateDbRead(s).rows;
277
+ }
278
+ /** `stateDbRows` and whether the read succeeded on a state.db whose messages carry ids (TAL-493). */
279
+ stateDbRead(s) {
188
280
  if (str(s.source_tag || s.raw_source || s.session_source).trim().toLowerCase() === 'subagent')
189
- return [];
190
- return stateDbSessionMessages(join(this.deps.profileHome(s.profile ?? this.deps.activeProfile()), 'state.db'), s.session_id, { stitch: false });
281
+ return { rows: [], idCapable: false, ok: false };
282
+ // TAL-529: after a compression rotation the Agent writes to the continuation, so the read follows the recorded lineage.
283
+ const lineage = s.state_db_lineage?.[0] === s.session_id ? s.state_db_lineage : null;
284
+ return stateDbSessionRead(this.stateDbPath(s.profile), s.session_id, { stitch: false, lineage });
285
+ }
286
+ stateDbPath(profile) {
287
+ return join(this.deps.profileHome(profile ?? this.deps.activeProfile()), 'state.db');
191
288
  }
192
289
  /**
193
290
  * Python `_merged_session_messages_for_display`: the sidecar transcript merged append-only with the Agent's state.db
@@ -197,7 +294,115 @@ export class SessionService {
197
294
  mergedTranscript(s, local = s.messages, stateRows = this.stateDbRows(s)) {
198
295
  if (!stateRows.length)
199
296
  return local;
200
- return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed });
297
+ return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed, stateDbSeenId: currentStateDbSeenId(s, stateRows) });
298
+ }
299
+ /**
300
+ * TAL-493: record the highest state.db id this boundary or settled turn read, under the session lock in the same save,
301
+ * so the merge never replays a covered row and appends every row committed after the read. Call it after the boundary
302
+ * fields are set: the marker holds only while they and the row it names stay as recorded.
303
+ */
304
+ markStateDbSeen(s, read = this.stateDbRead(s), opts = {}) {
305
+ // A failed read says nothing. A settlement keeps the marker; a boundary drops it (fail closed): it may cut rows the
306
+ // marker still admits without moving a field, and the timestamp rules honour its watermark.
307
+ if (!read.ok) {
308
+ if (opts.boundary) {
309
+ s.state_db_seen_id = null;
310
+ s.state_db_seen_boundary = null;
311
+ s.state_db_seen_stamp = null;
312
+ }
313
+ return;
314
+ }
315
+ // A successful read with no rows yet is a baseline: every row the session gets later is new.
316
+ s.state_db_seen_id = stateDbSeenId(read.rows) ?? (read.idCapable ? 0 : null);
317
+ s.state_db_seen_boundary = stateDbMarkKey(s, read.rows, s.state_db_seen_id);
318
+ s.state_db_seen_stamp = s.updated_at;
319
+ }
320
+ /**
321
+ * TAL-493: a turn's settlement. Each state.db row the merge shows now is matched to what the turn holds: a row already
322
+ * in the context it started from (by id), or one row the Agent added this turn (`agentRows` past that context, matched
323
+ * one to one by full text). An unmatched row is a CLI or gateway row committed while the turn ran: it joins the
324
+ * transcript and the model context before the marker passes it. Without the Agent's rows (`agentRows` empty), rows past
325
+ * `startId` (the highest id the turn read when it started) are its own unreported work: covered, never shown. Rows the
326
+ * turn started from but the transcript lacks (a CLI continuation read into its context) are kept before the turn.
327
+ */
328
+ settleStateDb(s, turn) {
329
+ const read = turn.read ?? this.stateDbRead(s);
330
+ const startedWith = new Set(turn.previousContext.flatMap((m) => (typeof m._state_db_row_id === 'number' ? [m._state_db_row_id] : [])));
331
+ const earlier = mergeSessionMessagesAppendOnly(s.messages, read.rows.filter((m) => typeof m._state_db_row_id === 'number' && startedWith.has(m._state_db_row_id)), { stateDbSeenId: -1 }).slice(s.messages.length);
332
+ const added = new Map();
333
+ for (const m of withoutRows(turn.agentRows ?? [], turn.previousContext))
334
+ added.set(settledIdentity(m), (added.get(settledIdentity(m)) ?? 0) + 1);
335
+ const ownWorkAfter = turn.agentRows?.length ? null : turn.startId ?? null;
336
+ // Without a current marker, the turn's own starting read is the baseline: rows committed after it are judged here.
337
+ const seenId = currentStateDbSeenId(s, read.rows) ?? turn.startId ?? null;
338
+ const shown = read.rows.length ? mergeSessionMessagesAppendOnly(s.messages, read.rows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed, stateDbSeenId: seenId }) : s.messages;
339
+ const fresh = new Set(shown.slice(s.messages.length));
340
+ // The turn's own rows the merge already matched to their local copies use up their share before any shown row can.
341
+ if (seenId !== null) {
342
+ for (const m of read.rows) {
343
+ const id = m._state_db_row_id;
344
+ if (typeof id !== 'number' || id <= seenId || startedWith.has(id) || fresh.has(m))
345
+ continue;
346
+ const left = added.get(settledIdentity(m)) ?? 0;
347
+ if (left > 0)
348
+ added.set(settledIdentity(m), left - 1);
349
+ }
350
+ }
351
+ const missed = [...fresh].filter((m) => {
352
+ const id = m._state_db_row_id;
353
+ if (typeof id === 'number' && startedWith.has(id))
354
+ return false;
355
+ const key = settledIdentity(m);
356
+ const left = added.get(key) ?? 0;
357
+ if (left > 0) {
358
+ added.set(key, left - 1);
359
+ return false;
360
+ }
361
+ return !(ownWorkAfter !== null && typeof id === 'number' && id > ownWorkAfter);
362
+ });
363
+ if (earlier.length) {
364
+ const at = s.messages.findIndex((m) => m._turn_id === turn.turnId);
365
+ s.messages.splice(at < 0 ? s.messages.length : at, 0, ...copyJson(earlier));
366
+ }
367
+ if (missed.length) {
368
+ s.messages.push(...copyJson(missed));
369
+ if (s.context_messages.length)
370
+ s.context_messages.push(...copyJson(missed));
371
+ }
372
+ this.markStateDbSeen(s, read);
373
+ // TAL-529: a compression rotation moved the Agent to a continuation, whose first rows restate the compressed context.
374
+ // The read follows the new lineage from here, and everything it holds now is covered.
375
+ // ponytail: a CLI row committed to the continuation before this settlement is covered with them; telling them apart
376
+ // needs the Agent to mark the rows it writes at rotation.
377
+ const lineage = stateDbCompressionLineage(this.stateDbPath(s.profile), s.session_id);
378
+ if (JSON.stringify(lineage) !== JSON.stringify(s.state_db_lineage ?? [s.session_id])) {
379
+ s.state_db_lineage = lineage.length > 1 ? lineage : null;
380
+ this.markStateDbSeen(s);
381
+ }
382
+ }
383
+ /**
384
+ * TAL-921: the model context a turn leaves when it ends without its settled result (an error, a Stop, a dead run), as a
385
+ * Stop's: the Agent's rows when they hold the turn's prompt, else its own rows among `stateRows` (the state.db rows
386
+ * committed after it started), then the prose that streamed past them. Null when it captured nothing the model can use.
387
+ */
388
+ unsettledTurnContext(turn) {
389
+ const reported = turn.agentRows && checkpointTurnStart(turn.agentRows, turn.msgText, turn.historyLength) !== null ? turn.agentRows : null;
390
+ const own = reported ? null : runStateDbRows(turn.stateRows, turn.msgText);
391
+ const checkpoint = reported ?? (own && [...turn.previousContext, ...own]);
392
+ const context = stoppedTurnContext(turn.previousContext, checkpoint, turn.prompt, turn.msgText, turn.streamedText, turn.historyLength);
393
+ if (!context)
394
+ return null;
395
+ if (!own || !checkpoint || checkpointTurnStart(checkpoint, turn.msgText, turn.historyLength) === null)
396
+ return dedupeContext(context);
397
+ // The context holds the turn's own state.db rows from its prompt on. The Agent persisted the prompt's text only, so the
398
+ // prompt row carries what the turn sent (attached files, images). This turn's settlement covers the rows: without
399
+ // their ids, the next one never reads them as a CLI continuation the transcript lacks.
400
+ const at = turn.previousContext.length;
401
+ return dedupeContext(context.map((m, i) => {
402
+ if (i === at)
403
+ return { role: 'user', content: turn.prompt, ...(m.timestamp != null ? { timestamp: m.timestamp } : {}) };
404
+ return i > at && i < at + own.length ? Object.fromEntries(Object.entries(m).filter(([k]) => k !== '_state_db_row_id' && k !== '_state_db_generation')) : m;
405
+ }));
201
406
  }
202
407
  /**
203
408
  * Python `reconciled_state_db_messages_for_session(prefer_context=True)`: the model history is the owner context
@@ -217,15 +422,30 @@ export class SessionService {
217
422
  const pending = this.pendingTurn(s);
218
423
  return pending ? withPendingUserTurn(context, pending) : context;
219
424
  }
425
+ /** Python `_is_messaging_session_id`: from the WebUI metadata or the Agent's row. */
426
+ isMessagingSession(sid) {
427
+ try {
428
+ if (isMessagingSessionRecord(this.store.get(sid, { metadataOnly: true }).compact()))
429
+ return true;
430
+ }
431
+ catch { /* absent */ }
432
+ const meta = this.lookupCliMeta(sid);
433
+ return meta !== null && isMessagingSessionRecord(meta);
434
+ }
220
435
  /** Python `_lookup_cli_session_metadata`: the sidebar row for a state.db session in the active profile. */
221
436
  lookupCliMeta(sid) {
222
437
  try {
223
- return this.deps.cliSessions(this.deps.activeProfile(), { sourceFilter: null }).rows.find((r) => str(r.session_id) === sid) ?? null;
438
+ if (isClaudeCodeSessionId(sid))
439
+ return this.claudeCodeRows().find((r) => str(r.session_id) === sid) ?? null;
440
+ return this.deps.cliSessionsSync(this.deps.activeProfile(), { sourceFilter: null }).rows.find((r) => str(r.session_id) === sid) ?? null;
224
441
  }
225
442
  catch {
226
443
  return null;
227
444
  }
228
445
  }
446
+ claudeCodeRows() {
447
+ return this.deps.claudeCode?.rows(this.deps.workspaces.lastWorkspace(this.deps.activeProfile())) ?? [];
448
+ }
229
449
  /** Python `_session_index_marks_was_webui`: an index row that once owned a WebUI sidecar (self-heal 404). */
230
450
  indexMarksWasWebui(sid) {
231
451
  let entries;
@@ -262,7 +482,7 @@ export class SessionService {
262
482
  const subagentChild = stateDbSource === 'subagent';
263
483
  if ((this.indexMarksWasWebui(sid) || (this.store.wasDeleted(sid) && ['', 'webui', 'fork'].includes(stateDbSource))) && !subagentChild)
264
484
  return { session: null, reason: 'was_webui' };
265
- const msgs = stateDbSessionMessages(dbPath, sid, { stitch: true });
485
+ const msgs = isClaudeCodeSessionId(sid) ? this.deps.claudeCode?.messages(sid) ?? [] : stateDbSessionMessages(dbPath, sid, { stitch: true });
266
486
  if (!msgs.length)
267
487
  return { session: null, reason: 'no_foreign_state' };
268
488
  const meta = { ...(metaIn ?? this.lookupCliMeta(sid) ?? {}) };
@@ -309,7 +529,16 @@ export class SessionService {
309
529
  throw error;
310
530
  }
311
531
  }
532
+ /**
533
+ * TAL-551: a Claude Code import is a live view of a transcript Claude Code keeps writing. Web never stores a copy,
534
+ * which would freeze its messages and pin it to one profile.
535
+ */
536
+ rejectClaudeCode(sid, verb) {
537
+ if (isClaudeCodeSessionId(sid))
538
+ throw new HttpFailure(400, `Claude Code sessions are view-only and cannot be ${verb} from WebUI`);
539
+ }
312
540
  rejectSubagent(sid, verb) {
541
+ this.rejectClaudeCode(sid, verb);
313
542
  if (this.isSubagentViewOnly(sid))
314
543
  throw new HttpFailure(400, `Subagent sessions are view-only and cannot be ${verb} from WebUI`);
315
544
  }
@@ -321,8 +550,10 @@ export class SessionService {
321
550
  publicSession(s, withMessages = true) {
322
551
  const payload = this.wireRow(s);
323
552
  // Mutation replies replace a client's transcript, so they carry the same server-built scenes as the detail.
324
- if (withMessages)
553
+ if (withMessages) {
325
554
  payload.messages = this.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withMarkerKinds(withTurnIds(s.messages)), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, runningScene: !this.journaledActiveTurn(s) }));
555
+ payload.compression_reference = compressionReference(s, payload.messages);
556
+ }
326
557
  return redactSessionData(payload, this.deps.redactEnabled());
327
558
  }
328
559
  /** `compact()` with the wire streaming/read-only flags (TAL-312), for replies that return the session row. */
@@ -385,7 +616,8 @@ export class SessionService {
385
616
  }
386
617
  this.clearStaleStreamState(s);
387
618
  const journaled = loadMessages ? this.journaledActiveTurn(s) : null;
388
- let transcript = loadMessages ? this.mergedTranscript(s) : [];
619
+ const stateRows = loadMessages ? this.stateDbRows(s) : [];
620
+ let transcript = loadMessages ? this.mergedTranscript(s, s.messages, stateRows) : [];
389
621
  const pending = loadMessages ? this.pendingTurn(s) : null;
390
622
  if (pending)
391
623
  transcript = withPendingUserTurn(transcript, pending);
@@ -442,8 +674,10 @@ export class SessionService {
442
674
  };
443
675
  if (loadMessages)
444
676
  raw.transcript_seq = journaled ? { stream_id: journaled.turnId, seq: 0 } : null;
445
- if (loadMessages && all.length)
446
- attachTodoState(raw, all);
677
+ if (loadMessages)
678
+ attachTodoState(raw, all, s.extra[UNSETTLED_TODO_KEY]);
679
+ if (loadMessages)
680
+ raw.compression_reference = compressionReference(s, all);
447
681
  if (mergedLast) {
448
682
  raw.last_message_at = Math.max(Number(raw.last_message_at ?? 0) || 0, mergedLast);
449
683
  raw.updated_at = Math.max(Number(raw.updated_at ?? 0) || 0, mergedLast);
@@ -457,10 +691,42 @@ export class SessionService {
457
691
  raw.read_only = this.isReadOnly(s);
458
692
  raw.assistant_name = this.assistantName(s);
459
693
  raw.workspace_name = this.workspaceNames()(s);
694
+ raw.branched_from = this.branchedFrom(s);
460
695
  withSessionWireFlags(raw, activeStreamIds);
696
+ // A metadata-only read lacks the history the gate counts, so it carries no flag.
697
+ if (loadMessages)
698
+ raw.can_compress = this.canCompress(s, raw.read_only === true, stateRows);
461
699
  raw.pending_steers = raw.active_stream_id ? (this.deps.runtime.pendingSteers?.(str(raw.active_stream_id)) ?? []) : [];
462
700
  return redactSessionData(raw, this.deps.redactEnabled());
463
701
  }
702
+ /**
703
+ * TAL-454: the chat `/branch` copied, while it still loads (archived included). A compression continuation is also a
704
+ * fork of its parent but not a branch, so it links nowhere.
705
+ */
706
+ branchedFrom(s) {
707
+ const parentId = s.parent_session_id;
708
+ if (!parentId || str(s.session_source).trim().toLowerCase() !== 'fork' || s.compression_recovery_source_session_id)
709
+ return null;
710
+ let title;
711
+ try {
712
+ const parent = this.store.get(parentId, { metadataOnly: true, promote: false, cacheOnMiss: false });
713
+ if (!this.visibleToActiveProfile(parent.profile))
714
+ return null;
715
+ title = parent.title;
716
+ }
717
+ catch {
718
+ // A read-only foreign parent (a cron run) has no sidecar and opens from the active profile's state.db while it
719
+ // has transcript rows and is not a deleted chat (`claimOrSynthesizeCliSession`'s `was_webui` and `no_foreign_state`).
720
+ const dbPath = this.stateDbPath(null);
721
+ const row = stateDbSessionRow(dbPath, parentId);
722
+ if (!row || this.store.wasDeleted(parentId) || this.indexMarksWasWebui(parentId))
723
+ return null;
724
+ if (!stateDbSessionMessages(dbPath, parentId, { stitch: true }).length)
725
+ return null;
726
+ title = row.title;
727
+ }
728
+ return { session_id: parentId, title: str(redactText(str(title) || 'Untitled', this.deps.redactEnabled())) };
729
+ }
464
730
  /**
465
731
  * TAL-316: the active run whose journal can replay its output from the start. The detail then leaves that output to the
466
732
  * replay; with no active run, no identifiable prompt, or no complete journal, it returns the persisted transcript unchanged.
@@ -518,7 +784,7 @@ export class SessionService {
518
784
  const merged = withSessionWireFlags(meta ? mergeCliSidebarMetadata(sess, meta) : sess, this.deps.runtime.activeStreamIds);
519
785
  return redactSessionData(merged, this.deps.redactEnabled());
520
786
  }
521
- /** Clear persisted streaming flags when no live stream backs them (Python `_clear_stale_stream_state`, no journal recovery). */
787
+ /** Clear persisted streaming flags when no live stream backs them (Python `_clear_stale_stream_state`). */
522
788
  clearStaleStreamState(session) {
523
789
  const streamId = session.active_stream_id;
524
790
  if (!streamId)
@@ -545,15 +811,41 @@ export class SessionService {
545
811
  return false;
546
812
  }
547
813
  }
548
- // Python `_materialize_pending_user_turn_before_error` (#1361): the prompt that was in flight becomes a durable user
549
- // turn and an interruption marker follows it, so a dead stream never silently drops what the user sent.
814
+ // Python `_materialize_pending_user_turn_before_error` (#1361) and `_recover_dead_run_journal`: the prompt that was in
815
+ // flight becomes a durable user turn, the output its journal holds follows it, and an interruption marker closes a
816
+ // run that never finished with an answer, so a dead stream never silently drops what the user sent or what streamed.
817
+ const turnId = str(target.active_stream_id);
818
+ // ponytail: one bounded tail read (Python's recovery window); a journal past it recovers its latest output only.
819
+ const events = this.deps.journal?.readRunEventTail(target.session_id, turnId, RECOVERY_JOURNAL_MAX_BYTES, RECOVERY_JOURNAL_MAX_ROWS).events ?? [];
820
+ const { rows: output, answered } = journalOutputRows(events, turnId);
550
821
  const pendingText = str(target.pending_user_message);
822
+ const attachments = [...target.pending_attachments];
823
+ // The run's starting read died with its worker: the state.db rows from before it started stand in for it. Its own
824
+ // rows open with its prompt and run to the next prompt; anything else another client wrote meanwhile stays.
825
+ const read = this.stateDbRead(target);
826
+ const runStart = target.pending_started_at || events[0]?.created_at || this.deps.now();
827
+ const before = read.rows.filter((m) => Number(m.timestamp) < runStart);
828
+ const runRows = read.rows.filter((m) => Number(m.timestamp) >= runStart);
829
+ const ownRows = target.hasPendingPrompt ? runStateDbRows(runRows, pendingText) : null;
830
+ // Without this turn's rows: an eager save already put its prompt in the transcript a fresh context falls back to.
831
+ const previousContext = this.modelContext(target, before).filter((m) => m._turn_id !== turnId);
551
832
  if (target.hasPendingPrompt) {
552
- const turnId = str(target.active_stream_id);
553
833
  const startedAt = typeof target.pending_started_at === 'number' && target.pending_started_at > 0 ? target.pending_started_at : this.deps.now();
554
- const attachments = [...target.pending_attachments];
555
- target.messages.push({ role: 'user', content: pendingText, timestamp: Math.trunc(startedAt), ...(attachments.length ? { attachments } : {}), _recovered: true, _source: target.pending_user_source ?? 'webui', ...(turnId ? { _turn_id: turnId } : {}) });
556
- target.messages.push({ role: 'assistant', content: '**Interrupted:** The reply was interrupted before it could be saved.', timestamp: Math.trunc(this.deps.now()), _error: true, ...(turnId ? { _turn_id: turnId } : {}) });
834
+ // An eager save already checkpointed this prompt as the turn's user row.
835
+ if (!target.messages.some((m) => m.role === 'user' && !m._steer && m._turn_id === turnId))
836
+ target.messages.push({ role: 'user', content: pendingText, timestamp: Math.trunc(startedAt), ...(attachments.length ? { attachments } : {}), _recovered: true, _source: target.pending_user_source ?? 'webui', _turn_id: turnId });
837
+ // The model context settles as a Stop's does: the Agent's own rows when it committed the prompt, else the prompt it
838
+ // was sent; then the prose that streamed past them.
839
+ const prompt = attachedFilesPrompt(pendingText, attachments);
840
+ const streamed = output.map((m) => str(m.content)).filter(Boolean).join('\n\n');
841
+ target.context_messages = this.unsettledTurnContext({ previousContext, prompt, msgText: pendingText, streamedText: streamed, historyLength: previousContext.length, stateRows: runRows }) ?? dedupeContext([...structuredClone(previousContext), { role: 'user', content: prompt }]);
842
+ }
843
+ if (target.hasPendingPrompt || output.length) {
844
+ target.messages.push(...output);
845
+ if (!answered)
846
+ target.messages.push({ role: 'assistant', content: '**Interrupted:** The reply was interrupted before it could be saved.', timestamp: Math.trunc(this.deps.now()), _error: true, _turn_id: turnId });
847
+ // Settled like a turn: the Agent's own rows are covered by the recovered turn; without them, every row past the run's start is.
848
+ this.settleStateDb(target, { turnId, previousContext, agentRows: ownRows, startId: stateDbSeenId(before) ?? (read.idCapable ? 0 : null), read });
557
849
  }
558
850
  target.active_stream_id = null;
559
851
  target.pending_user_message = null;
@@ -577,15 +869,18 @@ export class SessionService {
577
869
  return true;
578
870
  }
579
871
  // ── list / search ────────────────────────────────────────────────────────
580
- list(params) {
872
+ async list(params) {
581
873
  const activeProfile = this.deps.activeProfile();
582
874
  const wantState = params.showCliSessions || params.showCronSessions || params.showWebhookSessions || params.showKanbanSessions;
583
875
  // Python reads every profile's state.db under all_profiles; this port projects the active profile only.
584
- const cliRead = wantState ? this.deps.cliSessions(activeProfile, { sourceFilter: params.sourceFilter ?? null }) : undefined;
585
- const cliRows = cliRead?.rows;
876
+ const cliRead = wantState ? await this.deps.cliSessions(activeProfile, { sourceFilter: params.sourceFilter ?? null }) : undefined;
877
+ const claudeCodeRows = params.showCliSessions && params.showClaudeCodeSessions ? this.claudeCodeRows() : [];
878
+ const cliRows = cliRead ? [...cliRead.rows, ...claudeCodeRows] : undefined;
586
879
  const gatewayIdentity = loadGatewaySessionIdentityMap(join(this.deps.profileHome(activeProfile), 'sessions', 'sessions.json'));
587
880
  const truncatedSources = cliRead?.truncated;
588
- const payload = buildSessionListPayload(this.store, { ...params, ...(cliRows ? { cliRows } : {}), ...(truncatedSources ? { truncatedSources } : {}), gatewayIdentity, stateDbSources: this.stateDbSources, activeProfile, isolatedProfileMode: this.deps.isolatedProfileMode(), profilesMatch: this.deps.profilesMatch });
881
+ const webuiRows = allSessions(this.store, { sidebarMetadataOnly: true });
882
+ const lookups = await this.listStateDbLookups(webuiRows);
883
+ const payload = buildSessionListPayload(this.store, { ...params, ...(cliRows ? { cliRows } : {}), ...(truncatedSources ? { truncatedSources } : {}), gatewayIdentity, ...lookups, webuiRows, activeProfile, isolatedProfileMode: this.deps.isolatedProfileMode(), profilesMatch: this.deps.profilesMatch });
589
884
  return sessionListResponse(payload, this.deps.runtime, this.deps.redactEnabled(), this.deps.now(), this.workspaceNames());
590
885
  }
591
886
  search(q, opts) {
@@ -609,15 +904,16 @@ export class SessionService {
609
904
  const query = q.toLowerCase().trim();
610
905
  if (!query)
611
906
  return { sessions: sessions.map((s) => redactRow({ ...s })), all_profiles: opts.allProfiles, active_profile: activeProfile };
907
+ const terms = sessionSearchTerms(query);
612
908
  const results = [];
613
909
  for (const s of sessions) {
614
- if (str(s.title).toLowerCase().includes(query)) {
910
+ if (sessionSearchMatches(str(s.title), terms)) {
615
911
  results.push(redactRow({ ...s, match_type: 'title' }));
616
912
  continue;
617
913
  }
618
914
  if (!opts.content)
619
915
  continue;
620
- const hit = this.contentMatch(str(s.session_id), query, opts.depth);
916
+ const hit = this.contentMatch(str(s.session_id), terms, opts.depth);
621
917
  if (hit)
622
918
  results.push(redactRow({ ...s, ...hit }));
623
919
  }
@@ -628,8 +924,8 @@ export class SessionService {
628
924
  * archived, projection and order), narrowed to `projectId` (`none`: rows without a project), then matched on the
629
925
  * title, then the metadata the row shows (workspace path and name, model, provider, profile, source label), then message content.
630
926
  */
631
- sidebarSearch(q, params, opts) {
632
- let rows = this.list(params).body.sessions;
927
+ async sidebarSearch(q, params, opts) {
928
+ let rows = (await this.list(params)).body.sessions;
633
929
  if (opts.projectId === 'none')
634
930
  rows = rows.filter((r) => !r.project_id);
635
931
  else if (opts.projectId)
@@ -638,22 +934,23 @@ export class SessionService {
638
934
  const base = { all_profiles: params.allProfiles, active_profile: this.deps.activeProfile(), include_archived: params.includeArchived, sidebar_filtered: true };
639
935
  if (!query)
640
936
  return { ...base, sessions: rows };
937
+ const terms = sessionSearchTerms(query);
641
938
  const results = [];
642
939
  for (const row of rows) {
643
- if (str(row.title).toLowerCase().includes(query))
940
+ if (sessionSearchMatches(str(row.title), terms))
644
941
  results.push({ ...row, match_type: 'title' });
645
- else if (['workspace', 'workspace_name', 'model', 'model_provider', 'profile', 'source_label'].some((k) => str(row[k]).toLowerCase().includes(query)))
942
+ else if (['workspace', 'workspace_name', 'model', 'model_provider', 'profile', 'source_label'].some((k) => sessionSearchMatches(str(row[k]), terms)))
646
943
  results.push({ ...row, match_type: 'metadata' });
647
944
  else if (opts.content) {
648
- const hit = this.contentMatch(str(row.session_id), query, opts.depth);
945
+ const hit = this.contentMatch(str(row.session_id), terms, opts.depth);
649
946
  if (hit)
650
947
  results.push({ ...row, ...hit });
651
948
  }
652
949
  }
653
950
  return { ...base, sessions: results, query, count: results.length };
654
951
  }
655
- /** The first of a stored session's first `depth` messages (0: all) containing `query`, with its redacted excerpt. */
656
- contentMatch(sid, query, depth) {
952
+ /** The first of a stored session's first `depth` messages (0: all) containing every term, with its redacted excerpt. */
953
+ contentMatch(sid, terms, depth) {
657
954
  let sess;
658
955
  try {
659
956
  sess = this.store.get(sid, { promote: false, cacheOnMiss: false });
@@ -663,10 +960,10 @@ export class SessionService {
663
960
  }
664
961
  for (const m of depth ? sess.messages.slice(0, depth) : sess.messages) {
665
962
  const c = sessionSearchMessageText(m);
666
- if (!c.toLowerCase().includes(query))
963
+ if (!sessionSearchMatches(c, terms))
667
964
  continue;
668
965
  const item = { match_type: 'content' };
669
- const preview = sessionSearchPreview(c, query);
966
+ const preview = sessionSearchPreview(c, terms);
670
967
  if (preview)
671
968
  item.match_preview = redactText(preview, this.deps.redactEnabled());
672
969
  return item;
@@ -721,14 +1018,16 @@ export class SessionService {
721
1018
  const [workspace] = this.deps.workspaces.resolveImplicitWithRecovery(value, (p) => this.deps.workspaces.profileDefaultWorkspaceForBoot(p), profile);
722
1019
  return workspace;
723
1020
  }
1021
+ /** The server owns the names' normalization (TAL-631): trimmed, blanks dropped, and nothing left is the profile's defaults. */
724
1022
  validateToolsetsShape(toolsets) {
725
1023
  if (toolsets === null || toolsets === undefined)
726
1024
  return null;
727
- if (!Array.isArray(toolsets) || !toolsets.length)
728
- throw new HttpFailure(400, 'toolsets must be a non-empty list or null');
729
- if (!toolsets.every((t) => typeof t === 'string' && t))
730
- throw new HttpFailure(400, 'each toolset must be a non-empty string');
731
- return toolsets;
1025
+ if (!Array.isArray(toolsets))
1026
+ throw new HttpFailure(400, 'toolsets must be a list or null');
1027
+ if (!toolsets.every((t) => typeof t === 'string'))
1028
+ throw new HttpFailure(400, 'each toolset must be a string');
1029
+ const names = toolsets.map((t) => t.trim()).filter(Boolean);
1030
+ return names.length ? names : null;
732
1031
  }
733
1032
  create(body, opts = {}) {
734
1033
  // Python `new_session`: a body without a profile takes the request's active profile (cookie first).
@@ -771,7 +1070,8 @@ export class SessionService {
771
1070
  markSessionTitleGenerated(current);
772
1071
  this.store.save(current, { touchUpdatedAt: false });
773
1072
  });
774
- await this.deps.syncTitle(current);
1073
+ // An explicit regenerate is user-requested, so state.db takes it over a manual name.
1074
+ await this.deps.syncTitle(current, true);
775
1075
  this.publish(eventReason, current.profile, current.session_id);
776
1076
  return current;
777
1077
  }
@@ -781,7 +1081,7 @@ export class SessionService {
781
1081
  applySessionTitleRename(s, rawTitle);
782
1082
  this.store.save(s);
783
1083
  });
784
- await this.deps.syncTitle(s);
1084
+ await this.deps.syncTitle(s, true);
785
1085
  this.publish('session_rename', s.profile, s.session_id);
786
1086
  return { session: this.wireRow(s) };
787
1087
  }
@@ -923,6 +1223,7 @@ export class SessionService {
923
1223
  throw new HttpFailure(400, 'keep_count must be non-negative');
924
1224
  await this.store.withLock(sid, () => {
925
1225
  truncateSessionAtKeep(s, keep);
1226
+ this.markStateDbSeen(s, undefined, { boundary: true });
926
1227
  this.store.save(s);
927
1228
  });
928
1229
  this.deps.runtime.evictAgent(sid);
@@ -967,12 +1268,146 @@ export class SessionService {
967
1268
  compressionTarget(sid) {
968
1269
  this.rejectSubagent(sid, 'compressed');
969
1270
  const s = this.mutationTarget(sid, 'compressed');
1271
+ return { s, history: this.compressionHistory(s) };
1272
+ }
1273
+ /** The loaded session's own compression guards: no running turn and enough model history. */
1274
+ compressionHistory(s, stateRows) {
970
1275
  if (s.active_stream_id)
971
1276
  throw new HttpFailure(409, 'Session is still streaming; wait for the current turn to finish.');
972
- const history = sanitizeMessagesForApi(this.modelContext(s));
1277
+ const history = sanitizeMessagesForApi(this.modelContext(s, stateRows));
973
1278
  if (history.length < 4)
974
1279
  throw new HttpFailure(400, 'Not enough conversation to compress (need at least 4 messages).');
975
- return { s, history };
1280
+ return history;
1281
+ }
1282
+ /** TAL-614: whether a manual compression would start now: `compressionTarget`'s gates, with `readOnly` the wire flag. */
1283
+ canCompress(s, readOnly, stateRows) {
1284
+ if (readOnly || isClaudeCodeSessionId(s.session_id))
1285
+ return false;
1286
+ try {
1287
+ this.compressionHistory(s, stateRows);
1288
+ return true;
1289
+ }
1290
+ catch (error) {
1291
+ if (error instanceof HttpFailure)
1292
+ return false;
1293
+ throw error;
1294
+ }
1295
+ }
1296
+ /** TAL-258: the session's WebUI record (absent for a CLI or gateway session) and the profile whose state.db holds its rows. */
1297
+ handoffTarget(sid) {
1298
+ let local = null;
1299
+ try {
1300
+ local = this.store.get(sid);
1301
+ }
1302
+ catch { /* a foreign session without a WebUI record */ }
1303
+ const profile = local?.profile ?? this.deps.activeProfile();
1304
+ return { local, profile, dbPath: this.stateDbPath(profile) };
1305
+ }
1306
+ /** Python `_handle_conversation_rounds` (TAL-258): the session's rounds and whether the handoff dock is due. */
1307
+ conversationRounds(sid, since) {
1308
+ const rounds = countConversationRounds(this.handoffTarget(sid).dbPath, sid, since);
1309
+ return { ok: true, rounds, threshold: CONVERSATION_ROUND_THRESHOLD, should_show: rounds >= CONVERSATION_ROUND_THRESHOLD };
1310
+ }
1311
+ /**
1312
+ * Python `_handle_handoff_summary` (TAL-258): the session's main model summarizes its last 50 state.db messages after
1313
+ * `since`, and the summary is appended as a display-only `handoff_summary` tool row. A failed or cut-off answer falls
1314
+ * back to a deterministic local summary rather than an error.
1315
+ */
1316
+ async handoffSummary(sid, since) {
1317
+ this.rejectSubagent(sid, 'summarized');
1318
+ const { local, profile, dbPath } = this.handoffTarget(sid);
1319
+ const rounds = countConversationRounds(dbPath, sid, since);
1320
+ if (rounds < CONVERSATION_ROUND_THRESHOLD)
1321
+ throw new HttpFailure(400, 'Not enough conversation rounds to generate a summary.');
1322
+ // A running turn settles the transcript it started from, which would drop a card appended meanwhile.
1323
+ if (local?.active_stream_id)
1324
+ throw new HttpFailure(409, 'Session is still streaming; wait for the current turn to finish.');
1325
+ const all = stateDbSessionMessages(dbPath, sid, { stitch: true });
1326
+ const messages = (since === null ? all : all.filter((m) => { const ts = stateDbTimestampSeconds(m.timestamp); return ts !== null && ts > since; })).slice(-50);
1327
+ if (messages.length < 2)
1328
+ throw new HttpFailure(400, 'Not enough messages to summarize.');
1329
+ const sidecar = this.deps.sidecar?.() ?? null;
1330
+ await ensureAgentRuntimeCurrent(sidecar);
1331
+ const meta = local ? null : this.lookupCliMeta(sid);
1332
+ const generated = await this.generateHandoffSummary(sidecar, profile, local, meta, messages);
1333
+ const pick = (row, ...keys) => { for (const key of keys) {
1334
+ const v = str(row?.[key]).trim();
1335
+ if (v)
1336
+ return v;
1337
+ } return ''; };
1338
+ // Python `get_cli_sessions` never listed WebUI's own state.db rows, so they name no channel.
1339
+ const cliRow = meta ?? this.lookupCliMeta(sid);
1340
+ const channel = pick(local, 'source_label', 'raw_source', 'source_tag', 'session_source') || (str(cliRow?.raw_source || cliRow?.source).trim().toLowerCase() === 'webui' ? '' : pick(cliRow, 'source_label', 'raw_source', 'source_tag', 'source')) || null;
1341
+ await this.persistHandoffMarker(sid, profile, handoffMarker(sid, generated.summary, channel, rounds, generated.fallback, this.deps.now()));
1342
+ return { ok: true, summary: generated.summary, message_count: messages.length, rounds, fallback: generated.fallback, ...(generated.warning ? { warning: generated.warning } : {}) };
1343
+ }
1344
+ /** The main model's summary at 700 tokens, once more at 1400 when it was cut off, else the local fallback. */
1345
+ async generateHandoffSummary(sidecar, profile, local, meta, messages) {
1346
+ const fallback = (warning) => ({ summary: fallbackHandoffSummary(messages), fallback: true, ...(warning ? { warning: `Summary generation used local fallback: ${warning}` } : {}) });
1347
+ if (!sidecar)
1348
+ return fallback('the Agent sidecar is not running');
1349
+ const [model, provider] = local ? this.deps.modelStateFromRequest(local.model, local.model_provider, local.model_provider) : [str(meta?.model).trim() || null, null];
1350
+ const prompt = [{ role: 'system', content: HANDOFF_SYSTEM_PROMPT }, { role: 'user', content: `Conversation transcript:\n${handoffTranscript(messages)}` }];
1351
+ const ask = (maxTokens) => sidecar.call('aux.complete', { profile_home: this.deps.profileHome(profile), task: 'handoff_summary', messages: prompt, model: model ?? '', provider, max_tokens: maxTokens, temperature: 0.2 }, { timeoutMs: 60_000 });
1352
+ try {
1353
+ let result = await ask(700);
1354
+ if (completionIncomplete(result))
1355
+ result = await ask(1400);
1356
+ return completionIncomplete(result) ? fallback() : { summary: result.text, fallback: false };
1357
+ }
1358
+ catch (error) {
1359
+ if (error instanceof SidecarError && error.condition === 'agent_runtime_stale')
1360
+ throw staleRuntimeFailure(error);
1361
+ // No credential is not a failure: the predecessor answered with the local summary and no warning.
1362
+ if (error instanceof SidecarError && error.condition === 'credential_missing')
1363
+ return fallback();
1364
+ return fallback(sanitizePaths(error));
1365
+ }
1366
+ }
1367
+ /**
1368
+ * Python `_persist_handoff_summary`: a messaging session gets the marker in state.db and its WebUI record; any other
1369
+ * session in its WebUI record, or in state.db when it has none. Each store skips a marker its tail already holds, and
1370
+ * the session lock keeps two summaries from both passing that check. A card neither store saved answers 503.
1371
+ */
1372
+ async persistHandoffMarker(sid, profile, marker) {
1373
+ const card = messageHandoffPayload(marker);
1374
+ const toStateDb = async () => {
1375
+ if (sameHandoff(handoffPayload(stateDbTailToolContent(this.stateDbPath(profile), sid)), card))
1376
+ return true;
1377
+ const sidecar = this.deps.sidecar?.();
1378
+ if (!sidecar)
1379
+ return false;
1380
+ try {
1381
+ return (await sidecar.call('state_db.append_message', { profile_home: this.deps.profileHome(profile), session_id: sid, role: 'tool', content: str(marker.content), tool_name: 'handoff_summary', timestamp: Number(marker.timestamp) })).ok;
1382
+ }
1383
+ catch {
1384
+ return false;
1385
+ }
1386
+ };
1387
+ await this.store.withLock(sid, async () => {
1388
+ let live = null;
1389
+ try {
1390
+ live = this.store.get(sid);
1391
+ }
1392
+ catch { /* no WebUI record */ }
1393
+ // A turn that started while the summary was generated would drop the card when it settles.
1394
+ if (live?.active_stream_id)
1395
+ throw new HttpFailure(409, 'Session is still streaming; wait for the current turn to finish.');
1396
+ const toLocal = () => {
1397
+ if (!live)
1398
+ return false;
1399
+ if (sameHandoff(messageHandoffPayload(live.messages.at(-1)), card))
1400
+ return true;
1401
+ live.messages.push(marker);
1402
+ this.store.save(live);
1403
+ return true;
1404
+ };
1405
+ const messaging = this.isMessagingSession(sid);
1406
+ const saved = messaging ? [await toStateDb(), toLocal()].some(Boolean) : toLocal() || await toStateDb();
1407
+ // The caller may switch away on success, so a card saved nowhere is an error, not a summary.
1408
+ if (!saved)
1409
+ throw new HttpFailure(503, 'The handoff summary could not be saved; please retry.');
1410
+ });
976
1411
  }
977
1412
  /** The session's compression job; a deleted session's finished result is dropped rather than served. */
978
1413
  compressionJob(sid) {
@@ -1034,7 +1469,8 @@ export class SessionService {
1034
1469
  if (streamState(live) !== streamBefore)
1035
1470
  throw new HttpFailure(409, 'Session stream state changed during compression; please retry.');
1036
1471
  // One state.db read serves the check, the kept display rows, and the boundary.
1037
- const stateRows = this.stateDbRows(live);
1472
+ const read = this.stateDbRead(live);
1473
+ const stateRows = read.rows;
1038
1474
  if (transcriptKey(live, sanitizeMessagesForApi(this.modelContext(live, stateRows))) !== historyKey)
1039
1475
  throw new HttpFailure(409, 'Session was modified during compression; please retry.');
1040
1476
  // Rows the sidecar returns without a timestamp take the newest one this compression saw, never the clock: the
@@ -1065,6 +1501,7 @@ export class SessionService {
1065
1501
  live.truncation_watermark = truncationWatermarkFor(compressed);
1066
1502
  live.truncation_boundary = live.truncation_watermark;
1067
1503
  live.truncation_watermark_compressed = true;
1504
+ this.markStateDbSeen(live, read, { boundary: true });
1068
1505
  live.last_prompt_tokens = result.after_tokens;
1069
1506
  live.post_compression_context_tokens_estimate = result.after_tokens;
1070
1507
  this.store.save(live);
@@ -1126,6 +1563,7 @@ export class SessionService {
1126
1563
  s.pending_user_source = null;
1127
1564
  s.clear_generation = hadMessages ? randomUUID().replace(/-/g, '') : null;
1128
1565
  applySessionTitleRename(s, 'Untitled');
1566
+ this.markStateDbSeen(s, undefined, { boundary: true });
1129
1567
  this.store.save(s);
1130
1568
  if (hadMessages) {
1131
1569
  try {
@@ -1156,6 +1594,7 @@ export class SessionService {
1156
1594
  return { error: 'The last message has nothing to resend.' };
1157
1595
  const removed = history.length - lastUser;
1158
1596
  shrinkTo(s, lastUser);
1597
+ this.markStateDbSeen(s, undefined, { boundary: true });
1159
1598
  this.store.save(s);
1160
1599
  return { ok: true, last_user_text: lastUserText, last_user_prompt: lastUserPrompt, last_user_attachments: lastUserAttachments, removed_count: removed };
1161
1600
  });
@@ -1171,6 +1610,7 @@ export class SessionService {
1171
1610
  const removedText = extractText(history[lastUser]?.content);
1172
1611
  const removed = history.length - lastUser;
1173
1612
  shrinkTo(s, lastUser);
1613
+ this.markStateDbSeen(s, undefined, { boundary: true });
1174
1614
  this.store.save(s);
1175
1615
  const preview = removedText.length > 40 ? `${removedText.slice(0, 40)}...` : removedText;
1176
1616
  return { ok: true, removed_count: removed, removed_preview: preview };
@@ -1246,14 +1686,109 @@ export class SessionService {
1246
1686
  }
1247
1687
  return { session_id: branch.session_id, title, parent_session_id: source.session_id };
1248
1688
  }
1689
+ /** Python `read_session_lineage_report` on the active profile's state.db. */
1690
+ lineageReport(sid) {
1691
+ return stateDbLineageReport(this.stateDbPath(null), sid);
1692
+ }
1693
+ /** TAL-259: recovery reads the active profile's state.db, and a recovered sidecar joins that profile. */
1694
+ recoveryDeps() {
1695
+ return { store: this.store, stateDbPath: this.stateDbPath(null), profile: this.deps.activeProfile(), stampingSince: this.deps.recoveryStampingSince(), log: this.deps.log };
1696
+ }
1697
+ recoveryAudit() {
1698
+ return auditSessionRecovery(this.recoveryDeps());
1699
+ }
1700
+ recoveryRepairSafe() {
1701
+ return repairSafeSessionRecovery(this.recoveryDeps());
1702
+ }
1703
+ /**
1704
+ * Python `_handle_session_compression_recovery_start`: open a compression-exhausted session's focused continuation, an
1705
+ * empty fork keeping its workspace, model, profile, toolsets and worktree. A retry opens the oldest continuation
1706
+ * already started from it in the same profile; the source's lock makes concurrent starts converge on one child.
1707
+ */
1708
+ async compressionRecoveryStart(sid) {
1709
+ if (this.isSubagentViewOnly(sid))
1710
+ throw new HttpFailure(400, 'Subagent sessions are view-only and cannot start compression recovery from WebUI');
1711
+ const action = COMPRESSION_RECOVERY_ACTION_START_FOCUSED;
1712
+ let created = false;
1713
+ const child = await this.store.withLock(sid, () => {
1714
+ // Validated under the lock: a delete that won it leaves no source to fork.
1715
+ const source = this.store.get(sid);
1716
+ if (!this.visibleToActiveProfile(source.profile))
1717
+ throw new HttpFailure(404, 'Session not found');
1718
+ if (!compressionRecoveryPayload(source))
1719
+ throw new HttpFailure(409, 'Session does not have a compression recovery action.');
1720
+ const existing = this.compressionRecoveryChild(sid, action, source.profile);
1721
+ if (existing)
1722
+ return existing;
1723
+ const base = (source.title || 'Untitled').trim() || 'Untitled';
1724
+ const fresh = new Session({
1725
+ title: base.endsWith(' (focused continuation)') ? base : `${base} (focused continuation)`, workspace: source.workspace, model: source.model, model_provider: source.model_provider,
1726
+ project_id: source.project_id, profile: source.profile, session_source: 'fork', personality: source.personality, enabled_toolsets: copyJson(source.enabled_toolsets),
1727
+ context_length: source.context_length, threshold_tokens: source.threshold_tokens, gateway_routing: copyJson(source.gateway_routing), gateway_routing_history: copyJson(source.gateway_routing_history),
1728
+ parent_session_id: source.session_id, worktree_path: source.worktree_path, worktree_branch: source.worktree_branch, worktree_repo_root: source.worktree_repo_root, worktree_created_at: source.worktree_created_at,
1729
+ compression_recovery_source_session_id: sid, compression_recovery_action: action,
1730
+ // An empty model-facing transcript: the focused follow-up must not replay the exhausted context.
1731
+ messages: [], context_messages: [], composer_draft: { text: '', files: [] },
1732
+ }, { workspace: source.workspace, model: source.model });
1733
+ try {
1734
+ this.store.save(fresh);
1735
+ }
1736
+ catch (error) {
1737
+ throw new HttpFailure(500, `Failed to start compression recovery: ${sanitizePaths(error)}`);
1738
+ }
1739
+ this.store.touch(fresh);
1740
+ created = true;
1741
+ return fresh;
1742
+ });
1743
+ if (created)
1744
+ this.publish('session_compression_recovery', child.profile, child.session_id);
1745
+ return {
1746
+ ok: true, session: this.publicSession(child), source_session_id: sid, recommended_recovery_action: action,
1747
+ message: created ? 'Started a focused continuation. Describe the next narrow task to continue.' : 'Opened the existing focused continuation for this exhausted session.',
1748
+ };
1749
+ }
1750
+ /** Python `find_compression_recovery_session`: the oldest same-profile continuation started from `sid`, cached or persisted. */
1751
+ compressionRecoveryChild(sid, action, profile) {
1752
+ const matches = (s) => s.compression_recovery_source_session_id === sid && s.compression_recovery_action === action && this.deps.profilesMatch(s.profile, profile);
1753
+ const found = new Map();
1754
+ for (const s of this.store.sessions.values())
1755
+ if (matches(s))
1756
+ found.set(s.session_id, s);
1757
+ for (const id of this.store.persistedIds()) {
1758
+ if (found.has(id) || this.store.sessions.has(id))
1759
+ continue;
1760
+ try {
1761
+ const meta = this.store.get(id, { metadataOnly: true, promote: false, cacheOnMiss: false });
1762
+ if (matches(meta))
1763
+ found.set(id, meta);
1764
+ }
1765
+ catch { /* unreadable: not a match */ }
1766
+ }
1767
+ const [oldest] = [...found.values()].sort((a, b) => (a.created_at || 0) - (b.created_at || 0) || (a.updated_at || 0) - (b.updated_at || 0) || (a.session_id < b.session_id ? -1 : 1));
1768
+ return oldest ? this.store.get(oldest.session_id) : null;
1769
+ }
1770
+ /** The run that keeps `delete` answering 409, if any; stream status reports it so a draining client can wait on it (TAL-622). */
1771
+ activeRunBlocking(sid) {
1772
+ const live = this.deps.runtime.activeRunStream(sid);
1773
+ if (live)
1774
+ return live;
1775
+ try {
1776
+ const candidate = str(this.store.get(sid, { metadataOnly: true, promote: false, cacheOnMiss: false }).active_stream_id).trim();
1777
+ if (candidate && this.deps.runtime.activeStreamIds.has(candidate))
1778
+ return candidate;
1779
+ }
1780
+ catch { /* absent */ }
1781
+ return null;
1782
+ }
1249
1783
  async delete(sid) {
1250
1784
  if (!sid)
1251
1785
  throw new HttpFailure(400, 'session_id is required');
1252
1786
  if (!isSafeSessionId(sid))
1253
1787
  throw new HttpFailure(400, 'Invalid session_id');
1254
1788
  // Python: a read-only import (persisted sidecar flag, sidebar metadata, or a foreign state.db owner such as a
1255
- // Claude Code session) is never deleted — that would erase the owner's authoritative transcript.
1256
- if (this.isReadOnlyImport(sid))
1789
+ // Claude Code session) is never deleted — that would erase the owner's authoritative transcript. An orphan has none.
1790
+ const orphaned = this.isOrphanedImport(sid);
1791
+ if (!orphaned && this.isReadOnlyImport(sid))
1257
1792
  throw new HttpFailure(400, 'Read-only imported sessions cannot be deleted from WebUI');
1258
1793
  if (this.isSubagentViewOnly(sid))
1259
1794
  throw new HttpFailure(400, 'Subagent sessions are view-only and cannot be deleted from WebUI');
@@ -1273,29 +1808,12 @@ export class SessionService {
1273
1808
  hadSidecar = false;
1274
1809
  }
1275
1810
  // Python `_is_messaging_session_id`: decided before the JSON is gone, from WebUI metadata or the Agent's row.
1276
- const isMessaging = (() => { try {
1277
- if (isMessagingSessionRecord(this.store.get(sid, { metadataOnly: true }).compact()))
1278
- return true;
1279
- }
1280
- catch { /* absent */ } const meta = this.lookupCliMeta(sid); return meta !== null && isMessagingSessionRecord(meta); })();
1281
- const blocking = () => {
1282
- const live = this.deps.runtime.activeRunStream(sid);
1283
- if (live)
1284
- return live;
1285
- try {
1286
- const snapshot = this.store.get(sid, { metadataOnly: true });
1287
- const candidate = str(snapshot.active_stream_id).trim();
1288
- if (candidate && this.deps.runtime.activeStreamIds.has(candidate))
1289
- return candidate;
1290
- }
1291
- catch { /* absent */ }
1292
- return null;
1293
- };
1294
- if (blocking())
1811
+ const isMessaging = this.isMessagingSession(sid);
1812
+ if (this.activeRunBlocking(sid))
1295
1813
  throw new HttpFailure(409, 'Session has an active run; stop it before deleting');
1296
1814
  try {
1297
1815
  await this.store.withLock(sid, () => {
1298
- if (blocking())
1816
+ if (this.activeRunBlocking(sid))
1299
1817
  throw new HttpFailure(409, 'Session has an active run; stop it before deleting');
1300
1818
  // A public share outlives its session file, and revokeShare needs the session, so revoke it first.
1301
1819
  try {
@@ -1328,13 +1846,24 @@ export class SessionService {
1328
1846
  // Python: the Agent/CLI transcript in state.db goes too (else a claimable CLI row resurfaces in the sidebar), but a
1329
1847
  // messaging channel's memory is never erased from the WebUI; the actual outcome is reported.
1330
1848
  let stateDbCleanupFailed = false;
1331
- if (!isMessaging) {
1849
+ if (!isMessaging && !orphaned) {
1850
+ // TAL-529: the compression continuations go too, tip first; the Agent's delete keeps and detaches them otherwise.
1851
+ let lineage = [];
1332
1852
  try {
1333
- stateDbCleanupFailed = !(await this.deps.runtime.deleteCliSession(eventProfile, sid));
1853
+ lineage = stateDbCompressionLineage(this.stateDbPath(eventProfile), sid);
1334
1854
  }
1335
1855
  catch {
1336
1856
  stateDbCleanupFailed = true;
1337
1857
  }
1858
+ for (const id of lineage.reverse()) {
1859
+ try {
1860
+ if (!(await this.deps.runtime.deleteCliSession(eventProfile, id)))
1861
+ stateDbCleanupFailed = true;
1862
+ }
1863
+ catch {
1864
+ stateDbCleanupFailed = true;
1865
+ }
1866
+ }
1338
1867
  }
1339
1868
  this.publish('session_delete', eventProfile);
1340
1869
  // Only a sidecar session is ever published to the relay.
@@ -1603,6 +2132,7 @@ export class SessionService {
1603
2132
  }
1604
2133
  // ── shares ───────────────────────────────────────────────────────────────
1605
2134
  createShare(sid) {
2135
+ this.rejectClaudeCode(sid, 'shared');
1606
2136
  let s = this.get404(sid);
1607
2137
  if (!this.visibleToActiveProfile(s.profile))
1608
2138
  throw new HttpFailure(404, 'Session not found');
@@ -1621,7 +2151,7 @@ export class SessionService {
1621
2151
  this.publish('session_share_create', s.profile, sid);
1622
2152
  return {
1623
2153
  ok: true,
1624
- share: { token: meta.share_token, url: `/share/${meta.share_token}`, title: meta.share_title, message_count: meta.share_message_count, created_at: meta.share_created_at, updated_at: meta.share_updated_at },
2154
+ share: { token: meta.share_token, url: sharePath(meta.share_token) ?? '', title: meta.share_title, message_count: meta.share_message_count, created_at: meta.share_created_at, updated_at: meta.share_updated_at },
1625
2155
  session: this.publicSession(s),
1626
2156
  };
1627
2157
  }
@@ -1688,6 +2218,44 @@ function findLastUserIndex(history) {
1688
2218
  export function sanitizePaths(error) {
1689
2219
  return str(error?.message ?? error).replace(/(?:(?:\/[a-zA-Z0-9_.-]+)+|(?:[A-Z]:\\[^\s]+))/g, '<path>');
1690
2220
  }
2221
+ /** TAL-536: a dead run's own state.db rows: from its prompt to the next prompt (tool results and applied steers stay in it). */
2222
+ function runStateDbRows(rows, prompt) {
2223
+ const at = rows.findIndex((m) => looksLikeCurrentUserTurn(m, prompt));
2224
+ if (at < 0)
2225
+ return null;
2226
+ const toolResult = (m) => Array.isArray(m.content) && m.content.some((part) => isDict(part) && part.type === 'tool_result');
2227
+ const end = rows.findIndex((m, i) => i > at && m.role === 'user' && !toolResult(m) && agentSteerText(m) === null);
2228
+ return rows.slice(at, end < 0 ? undefined : end);
2229
+ }
2230
+ /** TAL-493: a row's full-text identity; one with no text (reasoning or media only) by its raw role, content, and reasoning. */
2231
+ function settledIdentity(m) {
2232
+ return messageIdentity(m, Infinity) ?? JSON.stringify([m.role ?? null, m.content ?? null, reasoningFieldsText(m)]);
2233
+ }
2234
+ /** `rows` minus one occurrence of each row of `base`, by `settledIdentity`. */
2235
+ function withoutRows(rows, base) {
2236
+ const left = new Map();
2237
+ for (const m of base)
2238
+ left.set(settledIdentity(m), (left.get(settledIdentity(m)) ?? 0) + 1);
2239
+ return rows.filter((m) => { const key = settledIdentity(m); const n = left.get(key) ?? 0; if (n > 0)
2240
+ left.set(key, n - 1); return n === 0; });
2241
+ }
2242
+ /**
2243
+ * TAL-493: what the state.db marker was recorded under: the boundary fields every boundary writer sets (an older release
2244
+ * moves them without the marker) and the role, timestamp, and database file identity of the row the marker names (a
2245
+ * recreated state.db, whose ids start over, has no such row).
2246
+ */
2247
+ function stateDbMarkKey(s, stateRows, seenId) {
2248
+ const anchor = seenId === null ? undefined : stateRows.find((m) => m._state_db_row_id === seenId);
2249
+ return JSON.stringify([s.truncation_watermark ?? null, s.truncation_boundary ?? null, s.intentional_shrink_generation ?? null, s.clear_generation ?? null, anchor ? [anchor.role ?? null, anchor.timestamp ?? null, anchor._state_db_generation ?? null] : null]);
2250
+ }
2251
+ /**
2252
+ * The recorded state.db marker while what it was recorded under still holds and this version wrote the session last (an
2253
+ * older release can apply a boundary that changes no field), else null (the timestamp rules apply).
2254
+ */
2255
+ function currentStateDbSeenId(s, stateRows) {
2256
+ const seenId = s.state_db_seen_id;
2257
+ return seenId !== null && s.state_db_seen_stamp === s.updated_at && s.state_db_seen_boundary === stateDbMarkKey(s, stateRows, seenId) ? seenId : null;
2258
+ }
1691
2259
  function truncationWatermarkFor(messages) {
1692
2260
  if (!messages.length)
1693
2261
  return 0.0;