@maudecode/talaria-web 1.15.0 → 1.17.2

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 (386) hide show
  1. package/_release.json +4 -4
  2. package/dist/api/automation-raw.js +6 -3
  3. package/dist/api/automation-raw.js.map +1 -1
  4. package/dist/api/automation-router.d.ts +158 -16
  5. package/dist/api/automation-router.js +12 -6
  6. package/dist/api/automation-router.js.map +1 -1
  7. package/dist/api/chat-router.d.ts +193 -6
  8. package/dist/api/chat-router.js +39 -16
  9. package/dist/api/chat-router.js.map +1 -1
  10. package/dist/api/git-router.d.ts +12 -1
  11. package/dist/api/git-router.js +60 -38
  12. package/dist/api/git-router.js.map +1 -1
  13. package/dist/api/raw-routes.d.ts +6 -0
  14. package/dist/api/raw-routes.js +35 -58
  15. package/dist/api/raw-routes.js.map +1 -1
  16. package/dist/api/router.d.ts +4 -2
  17. package/dist/api/router.js +8 -8
  18. package/dist/api/router.js.map +1 -1
  19. package/dist/api/sessions-router.d.ts +974 -42
  20. package/dist/api/sessions-router.js +87 -25
  21. package/dist/api/sessions-router.js.map +1 -1
  22. package/dist/api/settings-router.d.ts +254 -11
  23. package/dist/api/settings-router.js +68 -18
  24. package/dist/api/settings-router.js.map +1 -1
  25. package/dist/api/sse-routes.d.ts +11 -3
  26. package/dist/api/sse-routes.js +145 -65
  27. package/dist/api/sse-routes.js.map +1 -1
  28. package/dist/api/tools-raw.js +6 -2
  29. package/dist/api/tools-raw.js.map +1 -1
  30. package/dist/api/tools-router.d.ts +86 -9
  31. package/dist/api/tools-router.js +19 -15
  32. package/dist/api/tools-router.js.map +1 -1
  33. package/dist/app.d.ts +0 -8397
  34. package/dist/app.js +7 -2
  35. package/dist/app.js.map +1 -1
  36. package/dist/auth/gate.js +11 -4
  37. package/dist/auth/gate.js.map +1 -1
  38. package/dist/auth/oidc.d.ts +6 -2
  39. package/dist/auth/oidc.js +16 -11
  40. package/dist/auth/oidc.js.map +1 -1
  41. package/dist/auth/store.d.ts +14 -2
  42. package/dist/auth/store.js +26 -1
  43. package/dist/auth/store.js.map +1 -1
  44. package/dist/cli/dotenv.d.ts +1 -1
  45. package/dist/cli/dotenv.js +36 -3
  46. package/dist/cli/dotenv.js.map +1 -1
  47. package/dist/config/agent-config.d.ts +2 -0
  48. package/dist/config/agent-config.js +9 -0
  49. package/dist/config/agent-config.js.map +1 -1
  50. package/dist/http/addresses.d.ts +6 -0
  51. package/dist/http/addresses.js +26 -0
  52. package/dist/http/addresses.js.map +1 -1
  53. package/dist/http/context.d.ts +6 -3
  54. package/dist/http/context.js +10 -4
  55. package/dist/http/context.js.map +1 -1
  56. package/dist/links.d.ts +26 -0
  57. package/dist/links.js +60 -0
  58. package/dist/links.js.map +1 -0
  59. package/dist/onboarding.js +30 -7
  60. package/dist/onboarding.js.map +1 -1
  61. package/dist/profiles/profiles.js +1 -1
  62. package/dist/profiles/profiles.js.map +1 -1
  63. package/dist/providers/catalog.d.ts +167 -5
  64. package/dist/providers/catalog.js +662 -135
  65. package/dist/providers/catalog.js.map +1 -1
  66. package/dist/providers/tables.d.ts +3 -1
  67. package/dist/providers/tables.js +9 -4
  68. package/dist/providers/tables.js.map +1 -1
  69. package/dist/redact.d.ts +1 -0
  70. package/dist/redact.js +518 -15
  71. package/dist/redact.js.map +1 -1
  72. package/dist/runtime.js +79 -37
  73. package/dist/runtime.js.map +1 -1
  74. package/dist/sessions/anchor.d.ts +24 -3
  75. package/dist/sessions/anchor.js +94 -44
  76. package/dist/sessions/anchor.js.map +1 -1
  77. package/dist/sessions/background-tasks.d.ts +4 -1
  78. package/dist/sessions/background-tasks.js +7 -4
  79. package/dist/sessions/background-tasks.js.map +1 -1
  80. package/dist/sessions/claude-code.d.ts +19 -0
  81. package/dist/sessions/claude-code.js +250 -0
  82. package/dist/sessions/claude-code.js.map +1 -0
  83. package/dist/sessions/completions.d.ts +13 -1
  84. package/dist/sessions/completions.js +64 -12
  85. package/dist/sessions/completions.js.map +1 -1
  86. package/dist/sessions/compress.d.ts +14 -1
  87. package/dist/sessions/compress.js +52 -11
  88. package/dist/sessions/compress.js.map +1 -1
  89. package/dist/sessions/journal.d.ts +8 -0
  90. package/dist/sessions/journal.js +44 -10
  91. package/dist/sessions/journal.js.map +1 -1
  92. package/dist/sessions/list.d.ts +13 -5
  93. package/dist/sessions/list.js +56 -26
  94. package/dist/sessions/list.js.map +1 -1
  95. package/dist/sessions/merge.d.ts +122 -9
  96. package/dist/sessions/merge.js +544 -33
  97. package/dist/sessions/merge.js.map +1 -1
  98. package/dist/sessions/pending.d.ts +5 -0
  99. package/dist/sessions/pending.js +30 -10
  100. package/dist/sessions/pending.js.map +1 -1
  101. package/dist/sessions/service.d.ts +100 -4
  102. package/dist/sessions/service.js +567 -102
  103. package/dist/sessions/service.js.map +1 -1
  104. package/dist/sessions/session.d.ts +16 -1
  105. package/dist/sessions/session.js +29 -2
  106. package/dist/sessions/session.js.map +1 -1
  107. package/dist/sessions/shares.js +11 -2
  108. package/dist/sessions/shares.js.map +1 -1
  109. package/dist/sessions/state-db.d.ts +26 -4
  110. package/dist/sessions/state-db.js +126 -8
  111. package/dist/sessions/state-db.js.map +1 -1
  112. package/dist/sessions/store.js +6 -1
  113. package/dist/sessions/store.js.map +1 -1
  114. package/dist/sessions/streams.d.ts +10 -0
  115. package/dist/sessions/streams.js +14 -3
  116. package/dist/sessions/streams.js.map +1 -1
  117. package/dist/sessions/todo.d.ts +6 -1
  118. package/dist/sessions/todo.js +11 -4
  119. package/dist/sessions/todo.js.map +1 -1
  120. package/dist/sessions/tool-display.d.ts +12 -1
  121. package/dist/sessions/tool-display.js +76 -1
  122. package/dist/sessions/tool-display.js.map +1 -1
  123. package/dist/sessions/turn.d.ts +49 -10
  124. package/dist/sessions/turn.js +459 -185
  125. package/dist/sessions/turn.js.map +1 -1
  126. package/dist/settings.d.ts +20 -2
  127. package/dist/settings.js +49 -2
  128. package/dist/settings.js.map +1 -1
  129. package/dist/sidecar/fake.d.ts +2 -2
  130. package/dist/sidecar/fake.js +7 -2
  131. package/dist/sidecar/fake.js.map +1 -1
  132. package/dist/spa.js +2 -1
  133. package/dist/spa.js.map +1 -1
  134. package/dist/test/harness.js +2 -0
  135. package/dist/test/harness.js.map +1 -1
  136. package/dist/tools/commands.d.ts +15 -0
  137. package/dist/tools/commands.js +52 -0
  138. package/dist/tools/commands.js.map +1 -0
  139. package/dist/tools/crons.d.ts +4 -1
  140. package/dist/tools/crons.js +58 -11
  141. package/dist/tools/crons.js.map +1 -1
  142. package/dist/tools/insights.d.ts +8 -2
  143. package/dist/tools/insights.js +18 -2
  144. package/dist/tools/insights.js.map +1 -1
  145. package/dist/tools/kanban.d.ts +41 -1
  146. package/dist/tools/kanban.js +113 -2
  147. package/dist/tools/kanban.js.map +1 -1
  148. package/dist/tools/mcp.d.ts +2 -1
  149. package/dist/tools/mcp.js +16 -6
  150. package/dist/tools/mcp.js.map +1 -1
  151. package/dist/tools/terminal.d.ts +1 -0
  152. package/dist/tools/terminal.js +1 -1
  153. package/dist/tools/terminal.js.map +1 -1
  154. package/dist/tools/update-notifications.d.ts +1 -1
  155. package/dist/tools/update-notifications.js +1 -1
  156. package/dist/tools/update-notifications.js.map +1 -1
  157. package/dist/tools/updates.d.ts +15 -3
  158. package/dist/tools/updates.js +207 -41
  159. package/dist/tools/updates.js.map +1 -1
  160. package/dist/workspace/fs.d.ts +7 -3
  161. package/dist/workspace/fs.js +5 -1
  162. package/dist/workspace/fs.js.map +1 -1
  163. package/dist/workspace/git.d.ts +57 -10
  164. package/dist/workspace/git.js +302 -195
  165. package/dist/workspace/git.js.map +1 -1
  166. package/dist/workspace/media-refs.d.ts +13 -0
  167. package/dist/workspace/media-refs.js +165 -0
  168. package/dist/workspace/media-refs.js.map +1 -0
  169. package/dist/workspace/media.d.ts +19 -0
  170. package/dist/workspace/media.js +63 -2
  171. package/dist/workspace/media.js.map +1 -1
  172. package/dist/workspace/preview.d.ts +28 -0
  173. package/dist/workspace/preview.js +110 -0
  174. package/dist/workspace/preview.js.map +1 -0
  175. package/dist/workspace/rollback.d.ts +5 -4
  176. package/dist/workspace/rollback.js +12 -9
  177. package/dist/workspace/rollback.js.map +1 -1
  178. package/dist/workspace/upload.d.ts +2 -0
  179. package/dist/workspace/upload.js +4 -1
  180. package/dist/workspace/upload.js.map +1 -1
  181. package/dist/workspace/workspaces.d.ts +7 -1
  182. package/dist/workspace/workspaces.js +30 -21
  183. package/dist/workspace/workspaces.js.map +1 -1
  184. package/dist/workspace/worktrees.d.ts +2 -4
  185. package/dist/workspace/worktrees.js +28 -33
  186. package/dist/workspace/worktrees.js.map +1 -1
  187. package/package.json +4 -2
  188. package/sidecar/scripts/replay_sidecar.py +4 -2
  189. package/sidecar/talaria_sidecar/__init__.py +1 -1
  190. package/sidecar/talaria_sidecar/__main__.py +7 -1
  191. package/sidecar/talaria_sidecar/home.py +98 -2
  192. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  193. package/sidecar/talaria_sidecar/methods/aux.py +19 -3
  194. package/sidecar/talaria_sidecar/methods/chat.py +551 -61
  195. package/sidecar/talaria_sidecar/methods/commands.py +61 -33
  196. package/sidecar/talaria_sidecar/methods/config.py +14 -10
  197. package/sidecar/talaria_sidecar/methods/cron.py +10 -6
  198. package/sidecar/talaria_sidecar/methods/kanban.py +115 -15
  199. package/sidecar/talaria_sidecar/methods/oauth.py +439 -0
  200. package/sidecar/talaria_sidecar/methods/process.py +60 -1
  201. package/sidecar/talaria_sidecar/methods/profiles.py +12 -5
  202. package/sidecar/talaria_sidecar/methods/providers.py +161 -1
  203. package/sidecar/talaria_sidecar/methods/runtime.py +5 -0
  204. package/sidecar/talaria_sidecar/methods/text.py +18 -21
  205. package/sidecar/talaria_sidecar/methods/usage.py +264 -5
  206. package/static/dist/FILES.txt +126 -121
  207. package/static/dist/assets/AppShell-fb2td9zi.js +1 -0
  208. package/static/dist/assets/ChatPage-cj96kiy9.js +25 -0
  209. package/static/dist/assets/Dialog-kxcg7ard.js +1 -0
  210. package/static/dist/assets/ExtensionRoute-jf7h0754.js +1 -0
  211. package/static/dist/assets/HubRoute-gmanpd5y.js +2 -0
  212. package/static/dist/assets/{Markdown-dy8t4dy7.js → Markdown-mdwlvj3w.js} +125 -125
  213. package/static/dist/assets/RightPanel-ftvrjlcp.js +1 -0
  214. package/static/dist/assets/Select-bilaypej.js +1 -0
  215. package/static/dist/assets/SessionListPanel-og8n36qs.js +1 -0
  216. package/static/dist/assets/SettingsLayout-jonjefmj.js +1 -0
  217. package/static/dist/assets/States-nmejrwrm.js +1 -0
  218. package/static/dist/assets/Toaster-nnl8zkt9.js +1 -0
  219. package/static/dist/assets/_app-gdidzd94.js +1 -0
  220. package/static/dist/assets/_app.ext._extensionId-opyh9y0v.js +1 -0
  221. package/static/dist/assets/_app.index-hbarzbjx.js +1 -0
  222. package/static/dist/assets/_app.insights-luywlarz.js +1 -0
  223. package/static/dist/assets/_app.kanban-kincnbr8.js +1 -0
  224. package/static/dist/assets/_app.logs-oi2gr3gs.js +1 -0
  225. package/static/dist/assets/_app.memory-b6jfivt6.js +1 -0
  226. package/static/dist/assets/_app.profiles-lpvixmqp.js +1 -0
  227. package/static/dist/assets/{_app.session._sessionId-gyoh5ylx.js → _app.session._sessionId-bfd4h3id.js} +1 -1
  228. package/static/dist/assets/_app.settings-vowx3iq0.js +1 -0
  229. package/static/dist/assets/_app.settings._section-cnntg1tu.js +3 -0
  230. package/static/dist/assets/_app.skills-fb57lpau.js +1 -0
  231. package/static/dist/assets/_app.tasks-eszpx1q7.js +2 -0
  232. package/static/dist/assets/_app.todos-nr05ac6w.js +1 -0
  233. package/static/dist/assets/_app.workspaces-d2luomu6.js +1 -0
  234. package/static/dist/assets/{abnfDiagram-VCTEODGH-cjqletra.js → abnfDiagram-VCTEODGH-b2x1lqot.js} +1 -1
  235. package/static/dist/assets/architecture-7GRP2DOG-hd0o7bde.js +1 -0
  236. package/static/dist/assets/architectureDiagram-5GKGNRK7-h38o5l7b.js +36 -0
  237. package/static/dist/assets/{back-e4m6nsnm.js → back-bemaer09.js} +1 -1
  238. package/static/dist/assets/{blockDiagram-I7D4REHJ-b195z021.js → blockDiagram-I7D4REHJ-bqquxv2u.js} +1 -1
  239. package/static/dist/assets/{c4Diagram-7LVT6UL2-olu212k1.js → c4Diagram-7LVT6UL2-f3fb8w6z.js} +1 -1
  240. package/static/dist/assets/cancel-drd0g6d9.js +1 -0
  241. package/static/dist/assets/channel-cpdgf867.js +1 -0
  242. package/static/dist/assets/{chunk-4HAMMTFA-b8o1nufs.js → chunk-4HAMMTFA-bzfyyzp3.js} +1 -1
  243. package/static/dist/assets/{chunk-75Z2AOVW-l1seddto.js → chunk-75Z2AOVW-ly99rfx5.js} +1 -1
  244. package/static/dist/assets/{chunk-DU6HZSFF-lo5zg15v.js → chunk-DU6HZSFF-bg6ecfqf.js} +3 -3
  245. package/static/dist/assets/{chunk-F27PBJKO-dsdzrp16.js → chunk-F27PBJKO-e8k7mmqw.js} +1 -1
  246. package/static/dist/assets/{chunk-GMAD6QVW-f3fxqq8b.js → chunk-GMAD6QVW-cmalnoaj.js} +1 -1
  247. package/static/dist/assets/{chunk-GVQU2GXP-h8egxtf6.js → chunk-GVQU2GXP-gmif5o01.js} +1 -1
  248. package/static/dist/assets/{chunk-IMKFNOWR-iolwd8s0.js → chunk-IMKFNOWR-f1nwwh6u.js} +1 -1
  249. package/static/dist/assets/{chunk-L3NEJ4N5-b88bbtwi.js → chunk-L3NEJ4N5-oktfh5v7.js} +1 -1
  250. package/static/dist/assets/{chunk-OSK3NFVY-e3e7nr6s.js → chunk-OSK3NFVY-b1sdx96z.js} +1 -1
  251. package/static/dist/assets/{chunk-P2QGCYS3-mdc46rt1.js → chunk-P2QGCYS3-fhu4nzxt.js} +1 -1
  252. package/static/dist/assets/{chunk-POPQ4Y6H-kqlllrrm.js → chunk-POPQ4Y6H-em31rnef.js} +1 -1
  253. package/static/dist/assets/{chunk-PWAF6VOD-he585k2x.js → chunk-PWAF6VOD-bxhhyftw.js} +1 -1
  254. package/static/dist/assets/{chunk-SHT3W25Y-gh94y9a4.js → chunk-SHT3W25Y-fmrvwcav.js} +3 -3
  255. package/static/dist/assets/{chunk-SVP7TREG-euxp9y7e.js → chunk-SVP7TREG-lymukl2j.js} +1 -1
  256. package/static/dist/assets/{chunk-TICWLB2K-c8cxes1q.js → chunk-TICWLB2K-lmza6k23.js} +1 -1
  257. package/static/dist/assets/classDiagram-ZZMXUADV-ko09sguk.js +1 -0
  258. package/static/dist/assets/classDiagram-v2-VYDZK3BY-ko09sguk.js +1 -0
  259. package/static/dist/assets/{copied-kq0u3ht7.js → copied-ima3pysb.js} +1 -1
  260. package/static/dist/assets/copy-fw5b1pc6.js +1 -0
  261. package/static/dist/assets/{cynefin-OW5HDTMX-lxd44ux4.js → cynefin-OW5HDTMX-czlhbyhs.js} +1 -1
  262. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-njpiwat8.js → cynefinDiagram-5FMLGOSQ-gq7mvswb.js} +1 -1
  263. package/static/dist/assets/{dagre-GXQ25YYZ-nvgy6yju.js → dagre-GXQ25YYZ-cqdbf7dr.js} +4 -4
  264. package/static/dist/assets/{diagram-S7CK7UJ4-g98nyv72.js → diagram-S7CK7UJ4-o6arxa58.js} +3 -3
  265. package/static/dist/assets/{diagram-UQ7AKVKN-ovgm6lvv.js → diagram-UQ7AKVKN-nhnd68tz.js} +1 -1
  266. package/static/dist/assets/{diagram-VSXAHHWV-pb2bn2e4.js → diagram-VSXAHHWV-dk2sclzn.js} +1 -1
  267. package/static/dist/assets/{diagram-VX7I27RA-6j4mvfqo.js → diagram-VX7I27RA-n3k5nyx3.js} +1 -1
  268. package/static/dist/assets/{diagram-Z3DM3KII-dqmb982r.js → diagram-Z3DM3KII-m6q9cwdh.js} +1 -1
  269. package/static/dist/assets/{ebnfDiagram-PWID7BFC-ozbpzb6d.js → ebnfDiagram-PWID7BFC-lsa5641p.js} +1 -1
  270. package/static/dist/assets/endpoints-d33siokx.js +64 -0
  271. package/static/dist/assets/{erDiagram-RLTQ6QDP-hl7m8vua.js → erDiagram-RLTQ6QDP-fo2ckbe6.js} +1 -1
  272. package/static/dist/assets/eventmodeling-NTZA5JFV-emctlo64.js +1 -0
  273. package/static/dist/assets/flowDiagram-HODETNUW-bfi3moqa.js +1 -0
  274. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ompixqmh.js → ganttDiagram-EL5Y4UJY-nzqz6x9g.js} +1 -1
  275. package/static/dist/assets/{gitGraph-4MIJSDKK-b6vxwp9q.js → gitGraph-4MIJSDKK-gukvcxe6.js} +1 -1
  276. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-k98f57on.js → gitGraphDiagram-WWUBYQGX-lqhdp8f0.js} +1 -1
  277. package/static/dist/assets/{highlighted-body-KPVGNVTW-cd3ff6ge.js → highlighted-body-KPVGNVTW-igw6cp39.js} +1 -1
  278. package/static/dist/assets/index-cxq58xb1.js +11 -0
  279. package/static/dist/assets/index-hgtx08n5.css +2 -0
  280. package/static/dist/assets/{info-A6RAGUB7-hjcwjami.js → info-A6RAGUB7-gvu7a93l.js} +1 -1
  281. package/static/dist/assets/infoDiagram-27XIBGKW-m8t8g6w1.js +2 -0
  282. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-bxak4mwu.js → ishikawaDiagram-5VMMS53U-l07vkbgj.js} +1 -1
  283. package/static/dist/assets/{journeyDiagram-3NMN7TZE-khmihclr.js → journeyDiagram-3NMN7TZE-hybkgj8l.js} +1 -1
  284. package/static/dist/assets/{kanban-definition-UXKFOSKX-h1r32wdx.js → kanban-definition-UXKFOSKX-by13uvtn.js} +1 -1
  285. package/static/dist/assets/{lazyRouteComponent-b2eg0fws.js → lazyRouteComponent-af0lvgjx.js} +1 -1
  286. package/static/dist/assets/{line-friyjof3.js → line-osxq6fq7.js} +1 -1
  287. package/static/dist/assets/link-jyxicdhg.js +1 -0
  288. package/static/dist/assets/login-ec21t39x.js +1 -0
  289. package/static/dist/assets/mermaid-HWGCJPDP-gfl8z3ep.js +1 -0
  290. package/static/dist/assets/{mermaid-parser.core-f1gvpwrt.js → mermaid-parser.core-i91oxmou.js} +3 -3
  291. package/static/dist/assets/{mindmap-definition-YA3MSWOX-mt2oxwue.js → mindmap-definition-YA3MSWOX-m82mx8ww.js} +1 -1
  292. package/static/dist/assets/onboarding-btnltvyz.js +1 -0
  293. package/static/dist/assets/onboarding_model_label-dp7xffoh.js +1 -0
  294. package/static/dist/assets/{packet-AYTQ26CC-jik4ixlf.js → packet-AYTQ26CC-nbtgge2o.js} +1 -1
  295. package/static/dist/assets/{pegDiagram-XKGWAZYB-erxlv6cc.js → pegDiagram-XKGWAZYB-f1zxzga1.js} +1 -1
  296. package/static/dist/assets/{pie-WAS4IAKB-mnkoldfl.js → pie-WAS4IAKB-of8vuhxs.js} +1 -1
  297. package/static/dist/assets/{pieDiagram-E7YTZNPT-i7lwrada.js → pieDiagram-E7YTZNPT-o7y8ftio.js} +3 -3
  298. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-fr8ejdoy.js → quadrantDiagram-AXDQQJYC-iqiwia1t.js} +1 -1
  299. package/static/dist/assets/{radar-RG4KPBEZ-otarjte2.js → radar-RG4KPBEZ-delnsy2v.js} +1 -1
  300. package/static/dist/assets/{railroad-74A4TZTK-lw362oto.js → railroad-74A4TZTK-nspnyq9u.js} +1 -1
  301. package/static/dist/assets/railroad-abnf-HS5TGJTU-nst1qxyn.js +1 -0
  302. package/static/dist/assets/railroad-ebnf-LZEXJU2U-ci37x8qy.js +1 -0
  303. package/static/dist/assets/railroad-peg-WCYAUIDC-aimkk07z.js +1 -0
  304. package/static/dist/assets/{railroadDiagram-O6MQD6OU-esyn75pw.js → railroadDiagram-O6MQD6OU-z41tqx5z.js} +1 -1
  305. package/static/dist/assets/refresh-b4uams1i.js +1 -0
  306. package/static/dist/assets/refresh-cw-nt1dtsx4.js +1 -0
  307. package/static/dist/assets/{requirementDiagram-BXWQKSXE-cyhi5u29.js → requirementDiagram-BXWQKSXE-bdvif194.js} +1 -1
  308. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-jljpbo0h.js → sankeyDiagram-P5KCCOFB-h73e0rol.js} +1 -1
  309. package/static/dist/assets/{saved-do49si3v.js → saved-kg52csy7.js} +1 -1
  310. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-fz0dhlu7.js → sequenceDiagram-WJ2MYXX4-iv95mb1z.js} +1 -1
  311. package/static/dist/assets/{settings_section_extensions_title-b7lojn0o.js → settings_section_extensions_title-ej0iapxe.js} +1 -1
  312. package/static/dist/assets/share._token-f97e94r0.js +2 -0
  313. package/static/dist/assets/sliders-horizontal-fj18it47.js +1 -0
  314. package/static/dist/assets/sse-fwpqtzbg.js +1 -0
  315. package/static/dist/assets/{stateDiagram-D77RDMKH-kdnithdx.js → stateDiagram-D77RDMKH-k417ixmf.js} +1 -1
  316. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hwn4apct.js +1 -0
  317. package/static/dist/assets/{swimlanes-42K2YHIH-esd6gmz3.js → swimlanes-42K2YHIH-esola70w.js} +1 -1
  318. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d1h97oyg.js +8 -0
  319. package/static/dist/assets/{timeline-definition-24CTP7MA-g0qygccv.js → timeline-definition-24CTP7MA-getrz2y8.js} +1 -1
  320. package/static/dist/assets/todoStore-eginf9h4.js +1 -0
  321. package/static/dist/assets/{treeView-Q6P3EWNA-ndchzkuu.js → treeView-Q6P3EWNA-hc7ap1q7.js} +1 -1
  322. package/static/dist/assets/{treemap-WGGIJYW6-hwjxj0zz.js → treemap-WGGIJYW6-b7k1syy5.js} +1 -1
  323. package/static/dist/assets/tts-lxdxnmjd.js +1 -0
  324. package/static/dist/assets/{untitled-mbr6tuhu.js → untitled-ovpfthg4.js} +1 -1
  325. package/static/dist/assets/useForm-bcblv1rx.js +1 -0
  326. package/static/dist/assets/useLocale-fi5jb8ma.js +1 -0
  327. package/static/dist/assets/{useParams-lo9u33d1.js → useParams-lhg566ew.js} +1 -1
  328. package/static/dist/assets/{useSearch-lq1mr090.js → useSearch-lbrh6ejn.js} +1 -1
  329. package/static/dist/assets/{vennDiagram-4TSXK5OY-gubovd1r.js → vennDiagram-4TSXK5OY-n6pcy5qb.js} +1 -1
  330. package/static/dist/assets/{wardley-WFR3VGLG-ir3om65j.js → wardley-WFR3VGLG-lpa18vz1.js} +1 -1
  331. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-ce0or1ie.js → wardleyDiagram-VM6X3IG4-kpclwwg7.js} +2 -2
  332. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-dexylcjg.js → xychartDiagram-S5SC5T6Z-paa2n8y0.js} +1 -1
  333. package/static/dist/index.html +1 -1
  334. package/static/dist/sw.js +2 -2
  335. package/static/dist/assets/AppShell-orat1s43.js +0 -1
  336. package/static/dist/assets/ChatPage-ojhka3hy.js +0 -22
  337. package/static/dist/assets/HubRoute-gxgw1t33.js +0 -2
  338. package/static/dist/assets/RightPanel-c66ani1s.js +0 -1
  339. package/static/dist/assets/Select-fswf66up.js +0 -1
  340. package/static/dist/assets/SessionListPanel-goh5mtt8.js +0 -1
  341. package/static/dist/assets/SettingsLayout-n5d4wq4m.js +0 -1
  342. package/static/dist/assets/States-kxyu50at.js +0 -1
  343. package/static/dist/assets/Toaster-6zmbcqgg.js +0 -1
  344. package/static/dist/assets/_app-fowyruiu.js +0 -1
  345. package/static/dist/assets/_app.ext._extensionId-nvjyrwhy.js +0 -1
  346. package/static/dist/assets/_app.index-c9zv4y7w.js +0 -1
  347. package/static/dist/assets/_app.insights-jf12nofo.js +0 -1
  348. package/static/dist/assets/_app.kanban-lri5a4yu.js +0 -1
  349. package/static/dist/assets/_app.logs-md1p0zqo.js +0 -1
  350. package/static/dist/assets/_app.memory-bzvf2e28.js +0 -1
  351. package/static/dist/assets/_app.profiles-jpg7zmbn.js +0 -1
  352. package/static/dist/assets/_app.settings-mpuwlx4j.js +0 -1
  353. package/static/dist/assets/_app.settings._section-knz8rwoj.js +0 -1
  354. package/static/dist/assets/_app.skills-fwu1dkir.js +0 -1
  355. package/static/dist/assets/_app.tasks-h9gphtl3.js +0 -2
  356. package/static/dist/assets/_app.todos-hd1dnvrh.js +0 -1
  357. package/static/dist/assets/_app.workspaces-l8toyn5f.js +0 -1
  358. package/static/dist/assets/architecture-7GRP2DOG-cavnusaw.js +0 -1
  359. package/static/dist/assets/architectureDiagram-5GKGNRK7-k5a7v36n.js +0 -36
  360. package/static/dist/assets/channel-gozy3dp3.js +0 -1
  361. package/static/dist/assets/classDiagram-ZZMXUADV-n0vs2f2q.js +0 -1
  362. package/static/dist/assets/classDiagram-v2-VYDZK3BY-n0vs2f2q.js +0 -1
  363. package/static/dist/assets/endpoints-k8agnro8.js +0 -9
  364. package/static/dist/assets/eventmodeling-NTZA5JFV-j1y2ekec.js +0 -1
  365. package/static/dist/assets/flowDiagram-HODETNUW-g2cvcbm5.js +0 -1
  366. package/static/dist/assets/index-nfy0ezs6.css +0 -2
  367. package/static/dist/assets/index-nqed3oum.js +0 -11
  368. package/static/dist/assets/infoDiagram-27XIBGKW-fm06rj36.js +0 -2
  369. package/static/dist/assets/link-m90ahx3n.js +0 -1
  370. package/static/dist/assets/login-bw7m2fzo.js +0 -1
  371. package/static/dist/assets/matchContext-f8oy3lo7.js +0 -1
  372. package/static/dist/assets/mermaid-HWGCJPDP-njwcl62n.js +0 -1
  373. package/static/dist/assets/onboarding-ret37j1e.js +0 -1
  374. package/static/dist/assets/railroad-abnf-HS5TGJTU-m741owar.js +0 -1
  375. package/static/dist/assets/railroad-ebnf-LZEXJU2U-noa4fbia.js +0 -1
  376. package/static/dist/assets/railroad-peg-WCYAUIDC-o4bpw4nb.js +0 -1
  377. package/static/dist/assets/refresh-cw-k66keho6.js +0 -1
  378. package/static/dist/assets/search-j5ki6a9o.js +0 -1
  379. package/static/dist/assets/share._token-kg8w647b.js +0 -2
  380. package/static/dist/assets/sliders-horizontal-c4wya4dy.js +0 -1
  381. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-k2wg2645.js +0 -1
  382. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-n5kvjtgr.js +0 -8
  383. package/static/dist/assets/tabBuild-bjmyvmn5.js +0 -56
  384. package/static/dist/assets/todoStore-kjk5l0sy.js +0 -1
  385. package/static/dist/assets/useForm-ocxu460f.js +0 -1
  386. package/static/dist/assets/useLocale-fzy5jhg6.js +0 -1
@@ -3,20 +3,24 @@ 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';
7
- import { anchorSceneIntOrNull, hydrateAnchorActivityScenes, normalizeAnchorSceneMessageRef, readAnchorSceneRows, storeAnchorScene, withTurnIds } from './anchor.js';
8
- import { isSafeSessionId, lastMessageTimestamp, Session, titleFrom } from './session.js';
6
+ import { allSessions, buildSessionListPayload, CLI_IDENTITY_FIELDS, isClaimableCliSource, isMessagingSessionRecord, withOwnerLocks, withSessionWireFlags, lineageRootId, mergeCliSidebarMetadata, sessionListResponse, sessionSearchMatches, sessionSearchMessageText, sessionSearchPreview, sessionSearchTerms } from './list.js';
7
+ import { anchorSceneIntOrNull, fullToolResult, hydrateAnchorActivityScenes, normalizeAnchorSceneMessageRef, readAnchorSceneRows, storeAnchorScene, withTurnIds } from './anchor.js';
8
+ import { 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 { stateDbCompressionLineage, stateDbSessionMessages, stateDbSessionRead, stateDbSessionRow, stateDbSessionSources } from './state-db.js';
13
+ import { anchorMessageKey, anchorSummary, CompressionJobs, compressionReference, visibleMessagesForAnchor } from './compress.js';
13
14
  import { SidecarError } from '../sidecar/client.js';
14
- import { attachmentObjects, isContextCompressionMarker, mergeSessionMessagesAppendOnly, pendingUserRow, sanitizeMessagesForApi, withAttachmentObjects, withBodyExcerpts, withPendingUserTurn, withToolCallOutcomes, withoutRunningTurnOutput } from './merge.js';
15
+ import { agentSteerText, attachedFilesPrompt, attachmentObjects, dedupeContext, isContextCompressionMarker, journalOutputRows, looksLikeCurrentUserTurn, stoppedTurnContext, workspaceContextPrefix, mergeSessionMessagesAppendOnly, messageIdentity, pendingUserRow, reasoningFieldsText, sanitizeMessagesForApi, stateDbSeenId, stripWorkspacePrefix, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withSceneRowMedia, withPendingUserTurn, withToolCallOutcomes, withoutRunningTurnOutput } from './merge.js';
15
16
  import { withBackgroundUpdates } from './background-updates.js';
16
17
  import { withBackgroundLinks } from './background-tasks.js';
17
18
  import { messagesForLimitedPayload, messageWindowForDisplay, MAX_MSG_LIMIT, parseMsgLimit, toolCallsForMessageWindow } from './window.js';
18
19
  import { redactText } from '../redact.js';
20
+ import { workspaceDisplayName } from '../workspace/workspaces.js';
19
21
  import { buildShareSnapshot } from './shares.js';
22
+ import { mediaAnchorRoot, mediaRefPath, mediaTarget } from '../workspace/media.js';
23
+ import { projectMediaRefs } from '../workspace/media-refs.js';
20
24
  import { loadGatewaySessionIdentityMap } from './list.js';
21
25
  import { join } from 'node:path';
22
26
  export class HttpFailure extends Error {
@@ -47,6 +51,9 @@ export async function ensureAgentRuntimeCurrent(sidecar) {
47
51
  function staleRuntimeFailure(error) {
48
52
  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 } : {}) });
49
53
  }
54
+ /** TAL-536: the most of a dead run's journal stale-stream cleanup reads (Python's recovery window). */
55
+ const RECOVERY_JOURNAL_MAX_BYTES = 8 * 1024 * 1024;
56
+ const RECOVERY_JOURNAL_MAX_ROWS = 65536;
50
57
  const isDict = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
51
58
  export class SessionService {
52
59
  deps;
@@ -57,6 +64,37 @@ export class SessionService {
57
64
  this.compressionJobs = new CompressionJobs();
58
65
  }
59
66
  get store() { return this.deps.store; }
67
+ /**
68
+ * TAL-186: rewrites a session's media references to their URLs. A local path is rewritten only when `/api/media`
69
+ * serves it for this session, under the same resolution, so every rewritten reference loads.
70
+ */
71
+ mediaProjector(s) {
72
+ const media = this.deps.media;
73
+ const served = new Map();
74
+ const localUrl = (path) => {
75
+ if (!media?.localIo(s.profile))
76
+ return null;
77
+ if (served.has(path))
78
+ return served.get(path) ?? null;
79
+ let url = null;
80
+ try {
81
+ const target = mediaTarget(mediaRefPath(path));
82
+ if (mediaAnchorRoot(target, s, media.access))
83
+ url = `./api/media?${new URLSearchParams({ path: target, session_id: s.session_id }).toString()}`;
84
+ }
85
+ catch { /* an invalid path stays text */ }
86
+ served.set(path, url);
87
+ return url;
88
+ };
89
+ const workspace = s.workspace.trim() || null;
90
+ // A scene's final answer usually repeats its row's text: each distinct text is parsed once.
91
+ const projected = new Map();
92
+ return (text) => {
93
+ if (!projected.has(text))
94
+ projected.set(text, projectMediaRefs(text, { workspace, localUrl }));
95
+ return projected.get(text) ?? null;
96
+ };
97
+ }
60
98
  publish(reason, profile, sessionId) {
61
99
  this.deps.events.publish(reason, { profile: profile ?? null, sessionId: sessionId ?? null });
62
100
  }
@@ -151,9 +189,18 @@ export class SessionService {
151
189
  }
152
190
  /** The Agent's state.db rows for this session (empty for subagent views, which never merge). */
153
191
  stateDbRows(s) {
192
+ return this.stateDbRead(s).rows;
193
+ }
194
+ /** `stateDbRows` and whether the read succeeded on a state.db whose messages carry ids (TAL-493). */
195
+ stateDbRead(s) {
154
196
  if (str(s.source_tag || s.raw_source || s.session_source).trim().toLowerCase() === 'subagent')
155
- return [];
156
- return stateDbSessionMessages(join(this.deps.profileHome(s.profile ?? this.deps.activeProfile()), 'state.db'), s.session_id, { stitch: false });
197
+ return { rows: [], idCapable: false, ok: false };
198
+ // TAL-529: after a compression rotation the Agent writes to the continuation, so the read follows the recorded lineage.
199
+ const lineage = s.state_db_lineage?.[0] === s.session_id ? s.state_db_lineage : null;
200
+ return stateDbSessionRead(this.stateDbPath(s.profile), s.session_id, { stitch: false, lineage });
201
+ }
202
+ stateDbPath(profile) {
203
+ return join(this.deps.profileHome(profile ?? this.deps.activeProfile()), 'state.db');
157
204
  }
158
205
  /**
159
206
  * Python `_merged_session_messages_for_display`: the sidecar transcript merged append-only with the Agent's state.db
@@ -163,7 +210,91 @@ export class SessionService {
163
210
  mergedTranscript(s, local = s.messages, stateRows = this.stateDbRows(s)) {
164
211
  if (!stateRows.length)
165
212
  return local;
166
- return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark });
213
+ return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed, stateDbSeenId: currentStateDbSeenId(s, stateRows) });
214
+ }
215
+ /**
216
+ * TAL-493: record the highest state.db id this boundary or settled turn read, under the session lock in the same save,
217
+ * so the merge never replays a covered row and appends every row committed after the read. Call it after the boundary
218
+ * fields are set: the marker holds only while they and the row it names stay as recorded.
219
+ */
220
+ markStateDbSeen(s, read = this.stateDbRead(s), opts = {}) {
221
+ // A failed read says nothing. A settlement keeps the marker; a boundary drops it (fail closed): it may cut rows the
222
+ // marker still admits without moving a field, and the timestamp rules honour its watermark.
223
+ if (!read.ok) {
224
+ if (opts.boundary) {
225
+ s.state_db_seen_id = null;
226
+ s.state_db_seen_boundary = null;
227
+ s.state_db_seen_stamp = null;
228
+ }
229
+ return;
230
+ }
231
+ // A successful read with no rows yet is a baseline: every row the session gets later is new.
232
+ s.state_db_seen_id = stateDbSeenId(read.rows) ?? (read.idCapable ? 0 : null);
233
+ s.state_db_seen_boundary = stateDbMarkKey(s, read.rows, s.state_db_seen_id);
234
+ s.state_db_seen_stamp = s.updated_at;
235
+ }
236
+ /**
237
+ * TAL-493: a turn's settlement. Each state.db row the merge shows now is matched to what the turn holds: a row already
238
+ * in the context it started from (by id), or one row the Agent added this turn (`agentRows` past that context, matched
239
+ * one to one by full text). An unmatched row is a CLI or gateway row committed while the turn ran: it joins the
240
+ * transcript and the model context before the marker passes it. Without the Agent's rows (`agentRows` empty), rows past
241
+ * `startId` (the highest id the turn read when it started) are its own unreported work: covered, never shown. Rows the
242
+ * turn started from but the transcript lacks (a CLI continuation read into its context) are kept before the turn.
243
+ */
244
+ settleStateDb(s, turn) {
245
+ const read = this.stateDbRead(s);
246
+ const startedWith = new Set(turn.previousContext.flatMap((m) => (typeof m._state_db_row_id === 'number' ? [m._state_db_row_id] : [])));
247
+ 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);
248
+ const added = new Map();
249
+ for (const m of withoutRows(turn.agentRows ?? [], turn.previousContext))
250
+ added.set(settledIdentity(m), (added.get(settledIdentity(m)) ?? 0) + 1);
251
+ const ownWorkAfter = turn.agentRows?.length ? null : turn.startId ?? null;
252
+ // Without a current marker, the turn's own starting read is the baseline: rows committed after it are judged here.
253
+ const seenId = currentStateDbSeenId(s, read.rows) ?? turn.startId ?? null;
254
+ const shown = read.rows.length ? mergeSessionMessagesAppendOnly(s.messages, read.rows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed, stateDbSeenId: seenId }) : s.messages;
255
+ const fresh = new Set(shown.slice(s.messages.length));
256
+ // The turn's own rows the merge already matched to their local copies use up their share before any shown row can.
257
+ if (seenId !== null) {
258
+ for (const m of read.rows) {
259
+ const id = m._state_db_row_id;
260
+ if (typeof id !== 'number' || id <= seenId || startedWith.has(id) || fresh.has(m))
261
+ continue;
262
+ const left = added.get(settledIdentity(m)) ?? 0;
263
+ if (left > 0)
264
+ added.set(settledIdentity(m), left - 1);
265
+ }
266
+ }
267
+ const missed = [...fresh].filter((m) => {
268
+ const id = m._state_db_row_id;
269
+ if (typeof id === 'number' && startedWith.has(id))
270
+ return false;
271
+ const key = settledIdentity(m);
272
+ const left = added.get(key) ?? 0;
273
+ if (left > 0) {
274
+ added.set(key, left - 1);
275
+ return false;
276
+ }
277
+ return !(ownWorkAfter !== null && typeof id === 'number' && id > ownWorkAfter);
278
+ });
279
+ if (earlier.length) {
280
+ const at = s.messages.findIndex((m) => m._turn_id === turn.turnId);
281
+ s.messages.splice(at < 0 ? s.messages.length : at, 0, ...copyJson(earlier));
282
+ }
283
+ if (missed.length) {
284
+ s.messages.push(...copyJson(missed));
285
+ if (s.context_messages.length)
286
+ s.context_messages.push(...copyJson(missed));
287
+ }
288
+ this.markStateDbSeen(s, read);
289
+ // TAL-529: a compression rotation moved the Agent to a continuation, whose first rows restate the compressed context.
290
+ // The read follows the new lineage from here, and everything it holds now is covered.
291
+ // ponytail: a CLI row committed to the continuation before this settlement is covered with them; telling them apart
292
+ // needs the Agent to mark the rows it writes at rotation.
293
+ const lineage = stateDbCompressionLineage(this.stateDbPath(s.profile), s.session_id);
294
+ if (JSON.stringify(lineage) !== JSON.stringify(s.state_db_lineage ?? [s.session_id])) {
295
+ s.state_db_lineage = lineage.length > 1 ? lineage : null;
296
+ this.markStateDbSeen(s);
297
+ }
167
298
  }
168
299
  /**
169
300
  * Python `reconciled_state_db_messages_for_session(prefer_context=True)`: the model history is the owner context
@@ -174,15 +305,29 @@ export class SessionService {
174
305
  const local = s.context_messages.length ? s.context_messages : s.messages.filter((m) => !m._error && !m._partial);
175
306
  return local.some((m) => isContextCompressionMarker(m)) ? local : this.mergedTranscript(s, local, stateRows ?? this.stateDbRows(s));
176
307
  }
308
+ /**
309
+ * TAL-518: the model context a `/btw` clone inherits: the chat's history plus its running turn's prompt, which
310
+ * deferred save keeps out of `messages` and `context_messages` until settlement.
311
+ */
312
+ sideQuestionContext(s) {
313
+ const context = structuredClone(this.modelContext(s));
314
+ const pending = this.pendingTurn(s);
315
+ return pending ? withPendingUserTurn(context, pending) : context;
316
+ }
177
317
  /** Python `_lookup_cli_session_metadata`: the sidebar row for a state.db session in the active profile. */
178
318
  lookupCliMeta(sid) {
179
319
  try {
320
+ if (isClaudeCodeSessionId(sid))
321
+ return this.claudeCodeRows().find((r) => str(r.session_id) === sid) ?? null;
180
322
  return this.deps.cliSessions(this.deps.activeProfile(), { sourceFilter: null }).rows.find((r) => str(r.session_id) === sid) ?? null;
181
323
  }
182
324
  catch {
183
325
  return null;
184
326
  }
185
327
  }
328
+ claudeCodeRows() {
329
+ return this.deps.claudeCode?.rows(this.deps.workspaces.lastWorkspace(this.deps.activeProfile())) ?? [];
330
+ }
186
331
  /** Python `_session_index_marks_was_webui`: an index row that once owned a WebUI sidecar (self-heal 404). */
187
332
  indexMarksWasWebui(sid) {
188
333
  let entries;
@@ -219,7 +364,7 @@ export class SessionService {
219
364
  const subagentChild = stateDbSource === 'subagent';
220
365
  if ((this.indexMarksWasWebui(sid) || (this.store.wasDeleted(sid) && ['', 'webui', 'fork'].includes(stateDbSource))) && !subagentChild)
221
366
  return { session: null, reason: 'was_webui' };
222
- const msgs = stateDbSessionMessages(dbPath, sid, { stitch: true });
367
+ const msgs = isClaudeCodeSessionId(sid) ? this.deps.claudeCode?.messages(sid) ?? [] : stateDbSessionMessages(dbPath, sid, { stitch: true });
223
368
  if (!msgs.length)
224
369
  return { session: null, reason: 'no_foreign_state' };
225
370
  const meta = { ...(metaIn ?? this.lookupCliMeta(sid) ?? {}) };
@@ -266,7 +411,16 @@ export class SessionService {
266
411
  throw error;
267
412
  }
268
413
  }
414
+ /**
415
+ * TAL-551: a Claude Code import is a live view of a transcript Claude Code keeps writing. Web never stores a copy,
416
+ * which would freeze its messages and pin it to one profile.
417
+ */
418
+ rejectClaudeCode(sid, verb) {
419
+ if (isClaudeCodeSessionId(sid))
420
+ throw new HttpFailure(400, `Claude Code sessions are view-only and cannot be ${verb} from WebUI`);
421
+ }
269
422
  rejectSubagent(sid, verb) {
423
+ this.rejectClaudeCode(sid, verb);
270
424
  if (this.isSubagentViewOnly(sid))
271
425
  throw new HttpFailure(400, `Subagent sessions are view-only and cannot be ${verb} from WebUI`);
272
426
  }
@@ -278,13 +432,36 @@ export class SessionService {
278
432
  publicSession(s, withMessages = true) {
279
433
  const payload = this.wireRow(s);
280
434
  // Mutation replies replace a client's transcript, so they carry the same server-built scenes as the detail.
281
- if (withMessages)
282
- payload.messages = this.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withTurnIds(s.messages), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id }));
435
+ if (withMessages) {
436
+ 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) }));
437
+ payload.compression_reference = compressionReference(s, payload.messages);
438
+ }
283
439
  return redactSessionData(payload, this.deps.redactEnabled());
284
440
  }
285
441
  /** `compact()` with the wire streaming/read-only flags (TAL-312), for replies that return the session row. */
286
442
  wireRow(s) {
287
- return withSessionWireFlags({ ...s.compact({ contextLengthFor: this.deps.contextLengthFor }), read_only: this.isReadOnly(s), assistant_name: this.assistantName(s) }, this.deps.runtime.activeStreamIds);
443
+ return withSessionWireFlags({ ...s.compact({ contextLengthFor: this.deps.contextLengthFor, modelOptionFor: this.deps.modelOptionFor }), read_only: this.isReadOnly(s), assistant_name: this.assistantName(s), workspace_name: this.workspaceNames()(s) }, this.deps.runtime.activeStreamIds);
444
+ }
445
+ /**
446
+ * TAL-303: the label for a session's workspace, from its own profile's registry (a profileless row is the root's).
447
+ * One resolver serves one response, so each profile's registry is read once however many rows it names.
448
+ */
449
+ workspaceNames() {
450
+ const registries = new Map();
451
+ return (row) => {
452
+ const profile = str(row.profile) || 'default';
453
+ let entries = registries.get(profile);
454
+ if (!entries) {
455
+ try {
456
+ entries = this.deps.workspaces.entries(profile);
457
+ }
458
+ catch {
459
+ entries = [];
460
+ }
461
+ registries.set(profile, entries);
462
+ }
463
+ return workspaceDisplayName(str(row.workspace), entries);
464
+ };
288
465
  }
289
466
  /** The agent's display name for a session's profile (`assistant_name`). */
290
467
  assistantName(s) {
@@ -328,7 +505,7 @@ export class SessionService {
328
505
  if (journaled)
329
506
  transcript = withoutRunningTurnOutput(transcript, { ...journaled, localCount: s.messages.length });
330
507
  // Turn ids, tool outcomes and scenes are computed over the full transcript, so every window reports the same values.
331
- const all = loadMessages ? withBodyExcerpts(this.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withTurnIds(withAttachmentObjects(transcript)), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, clipToolResults: msgLimit !== null })), s.active_stream_id) : [];
508
+ const all = loadMessages ? this.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withMarkerKinds(withTurnIds(withAttachmentObjects(transcript))), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, runningScene: !journaled, clipToolResults: msgLimit !== null })) : [];
332
509
  let truncated = [];
333
510
  let offset = 0;
334
511
  let summaryCount = null;
@@ -344,6 +521,8 @@ export class SessionService {
344
521
  truncated = all.slice(prompt, offset + truncated.length);
345
522
  offset = prompt;
346
523
  }
524
+ // Per-row display fields (TAL-186 media, TAL-456 excerpts cut from that display text) only for the rows sent.
525
+ truncated = withBodyExcerpts(withDisplayMedia(truncated, this.mediaProjector(s)), s.active_stream_id);
347
526
  if (msgLimit !== null)
348
527
  truncated = messagesForLimitedPayload(truncated);
349
528
  }
@@ -362,7 +541,7 @@ export class SessionService {
362
541
  }
363
542
  const activeStreamIds = this.deps.runtime.activeStreamIds;
364
543
  const raw = {
365
- ...s.compact({ includeRuntime: true, activeStreamIds, contextLengthFor: this.deps.contextLengthFor }),
544
+ ...s.compact({ includeRuntime: true, activeStreamIds, contextLengthFor: this.deps.contextLengthFor, modelOptionFor: this.deps.modelOptionFor }),
366
545
  messages: truncated,
367
546
  message_count: mergedCount,
368
547
  tool_calls: toolCalls,
@@ -376,8 +555,10 @@ export class SessionService {
376
555
  };
377
556
  if (loadMessages)
378
557
  raw.transcript_seq = journaled ? { stream_id: journaled.turnId, seq: 0 } : null;
379
- if (loadMessages && all.length)
380
- attachTodoState(raw, all);
558
+ if (loadMessages)
559
+ attachTodoState(raw, all, s.extra[UNSETTLED_TODO_KEY]);
560
+ if (loadMessages)
561
+ raw.compression_reference = compressionReference(s, all);
381
562
  if (mergedLast) {
382
563
  raw.last_message_at = Math.max(Number(raw.last_message_at ?? 0) || 0, mergedLast);
383
564
  raw.updated_at = Math.max(Number(raw.updated_at ?? 0) || 0, mergedLast);
@@ -390,6 +571,7 @@ export class SessionService {
390
571
  raw._load_revision = revisionBefore !== null && revisionBefore === revisionAfter ? hashRevision(revisionBefore) : `unstable-${randomUUID().replace(/-/g, '')}`;
391
572
  raw.read_only = this.isReadOnly(s);
392
573
  raw.assistant_name = this.assistantName(s);
574
+ raw.workspace_name = this.workspaceNames()(s);
393
575
  withSessionWireFlags(raw, activeStreamIds);
394
576
  raw.pending_steers = raw.active_stream_id ? (this.deps.runtime.pendingSteers?.(str(raw.active_stream_id)) ?? []) : [];
395
577
  return redactSessionData(raw, this.deps.redactEnabled());
@@ -437,7 +619,7 @@ export class SessionService {
437
619
  foreignSessionDetail(sid) {
438
620
  const { synth, meta } = this.foreignSession(sid);
439
621
  // The same turn projection as a WebUI session: turn ids, tool outcomes, then each completed turn's scene.
440
- const msgs = hydrateAnchorActivityScenes(withToolCallOutcomes(withTurnIds(synth.messages), [], null), {});
622
+ const msgs = hydrateAnchorActivityScenes(withToolCallOutcomes(withMarkerKinds(withTurnIds(synth.messages)), [], null), {});
441
623
  const lastTs = Number(msgs[msgs.length - 1]?.timestamp ?? 0) || 0;
442
624
  const sess = {
443
625
  session_id: synth.session_id, title: synth.title, workspace: synth.workspace, model: synth.model, message_count: msgs.length,
@@ -445,13 +627,13 @@ export class SessionService {
445
627
  pinned: synth.pinned, archived: synth.archived, project_id: synth.project_id ?? null, profile: synth.profile,
446
628
  is_cli_session: synth.is_cli_session, source_tag: synth.source_tag, raw_source: synth.raw_source, session_source: synth.session_source,
447
629
  source_label: synth.source_label, read_only: synth.read_only, can_duplicate: false, messages: msgs, tool_calls: [], transcript_seq: null,
448
- assistant_name: this.assistantName(synth),
630
+ assistant_name: this.assistantName(synth), workspace_name: this.workspaceNames()(synth),
449
631
  };
450
632
  attachTodoState(sess, msgs);
451
633
  const merged = withSessionWireFlags(meta ? mergeCliSidebarMetadata(sess, meta) : sess, this.deps.runtime.activeStreamIds);
452
634
  return redactSessionData(merged, this.deps.redactEnabled());
453
635
  }
454
- /** Clear persisted streaming flags when no live stream backs them (Python `_clear_stale_stream_state`, no journal recovery). */
636
+ /** Clear persisted streaming flags when no live stream backs them (Python `_clear_stale_stream_state`). */
455
637
  clearStaleStreamState(session) {
456
638
  const streamId = session.active_stream_id;
457
639
  if (!streamId)
@@ -478,15 +660,40 @@ export class SessionService {
478
660
  return false;
479
661
  }
480
662
  }
481
- // Python `_materialize_pending_user_turn_before_error` (#1361): the prompt that was in flight becomes a durable user
482
- // turn and an interruption marker follows it, so a dead stream never silently drops what the user sent.
663
+ // Python `_materialize_pending_user_turn_before_error` (#1361) and `_recover_dead_run_journal`: the prompt that was in
664
+ // flight becomes a durable user turn, the output its journal holds follows it, and an interruption marker closes a
665
+ // run that never finished with an answer, so a dead stream never silently drops what the user sent or what streamed.
666
+ const turnId = str(target.active_stream_id);
667
+ // ponytail: one bounded tail read (Python's recovery window); a journal past it recovers its latest output only.
668
+ const events = this.deps.journal?.readRunEventTail(target.session_id, turnId, RECOVERY_JOURNAL_MAX_BYTES, RECOVERY_JOURNAL_MAX_ROWS).events ?? [];
669
+ const { rows: output, answered } = journalOutputRows(events, turnId);
483
670
  const pendingText = str(target.pending_user_message);
671
+ const attachments = [...target.pending_attachments];
672
+ // The run's starting read died with its worker: the state.db rows from before it started stand in for it. Its own
673
+ // rows open with its prompt and run to the next prompt; anything else another client wrote meanwhile stays.
674
+ const read = this.stateDbRead(target);
675
+ const runStart = target.pending_started_at || events[0]?.created_at || this.deps.now();
676
+ const before = read.rows.filter((m) => Number(m.timestamp) < runStart);
677
+ const ownRows = target.hasPendingPrompt ? runStateDbRows(read.rows.filter((m) => Number(m.timestamp) >= runStart), pendingText) : null;
678
+ // Without this turn's rows: an eager save already put its prompt in the transcript a fresh context falls back to.
679
+ const previousContext = this.modelContext(target, before).filter((m) => m._turn_id !== turnId);
484
680
  if (target.hasPendingPrompt) {
485
- const turnId = str(target.active_stream_id);
486
681
  const startedAt = typeof target.pending_started_at === 'number' && target.pending_started_at > 0 ? target.pending_started_at : this.deps.now();
487
- const attachments = [...target.pending_attachments];
488
- 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 } : {}) });
489
- 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 } : {}) });
682
+ // An eager save already checkpointed this prompt as the turn's user row.
683
+ if (!target.messages.some((m) => m.role === 'user' && !m._steer && m._turn_id === turnId))
684
+ 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 });
685
+ // The model context settles as a Stop's does: the Agent's own rows when it committed the prompt, else the prompt it
686
+ // was sent; then the prose that streamed past them.
687
+ const prompt = attachedFilesPrompt((str(target.workspace) ? workspaceContextPrefix(str(target.workspace)) : '') + pendingText, attachments);
688
+ const streamed = output.map((m) => str(m.content)).filter(Boolean).join('\n\n');
689
+ target.context_messages = dedupeContext(stoppedTurnContext(previousContext, ownRows ? [...previousContext, ...ownRows] : null, prompt, pendingText, streamed, previousContext.length) ?? [...structuredClone(previousContext), { role: 'user', content: prompt }]);
690
+ }
691
+ if (target.hasPendingPrompt || output.length) {
692
+ target.messages.push(...output);
693
+ if (!answered)
694
+ 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 });
695
+ // 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.
696
+ this.settleStateDb(target, { turnId, previousContext, agentRows: ownRows, startId: stateDbSeenId(before) ?? (read.idCapable ? 0 : null) });
490
697
  }
491
698
  target.active_stream_id = null;
492
699
  target.pending_user_message = null;
@@ -515,11 +722,12 @@ export class SessionService {
515
722
  const wantState = params.showCliSessions || params.showCronSessions || params.showWebhookSessions || params.showKanbanSessions;
516
723
  // Python reads every profile's state.db under all_profiles; this port projects the active profile only.
517
724
  const cliRead = wantState ? this.deps.cliSessions(activeProfile, { sourceFilter: params.sourceFilter ?? null }) : undefined;
518
- const cliRows = cliRead?.rows;
725
+ const claudeCodeRows = params.showCliSessions && params.showClaudeCodeSessions ? this.claudeCodeRows() : [];
726
+ const cliRows = cliRead ? [...cliRead.rows, ...claudeCodeRows] : undefined;
519
727
  const gatewayIdentity = loadGatewaySessionIdentityMap(join(this.deps.profileHome(activeProfile), 'sessions', 'sessions.json'));
520
728
  const truncatedSources = cliRead?.truncated;
521
729
  const payload = buildSessionListPayload(this.store, { ...params, ...(cliRows ? { cliRows } : {}), ...(truncatedSources ? { truncatedSources } : {}), gatewayIdentity, stateDbSources: this.stateDbSources, activeProfile, isolatedProfileMode: this.deps.isolatedProfileMode(), profilesMatch: this.deps.profilesMatch });
522
- return sessionListResponse(payload, this.deps.runtime, this.deps.redactEnabled(), this.deps.now());
730
+ return sessionListResponse(payload, this.deps.runtime, this.deps.redactEnabled(), this.deps.now(), this.workspaceNames());
523
731
  }
524
732
  search(q, opts) {
525
733
  const activeProfile = this.deps.activeProfile();
@@ -528,8 +736,10 @@ export class SessionService {
528
736
  sessions = sessions.filter((r) => this.deps.profilesMatch(str(r.profile) || null, activeProfile));
529
737
  sessions = withOwnerLocks(sessions, this.stateDbSources);
530
738
  const redact = this.deps.redactEnabled();
739
+ const workspaceName = this.workspaceNames();
531
740
  const redactRow = (item) => {
532
741
  withSessionWireFlags(item, this.deps.runtime.activeStreamIds);
742
+ item.workspace_name = workspaceName(item);
533
743
  if (typeof item.title === 'string')
534
744
  item.title = redactText(item.title, redact);
535
745
  for (const f of ['display_title', '_state_db_title', 'parent_title'])
@@ -540,35 +750,71 @@ export class SessionService {
540
750
  const query = q.toLowerCase().trim();
541
751
  if (!query)
542
752
  return { sessions: sessions.map((s) => redactRow({ ...s })), all_profiles: opts.allProfiles, active_profile: activeProfile };
753
+ const terms = sessionSearchTerms(query);
543
754
  const results = [];
544
755
  for (const s of sessions) {
545
- if (str(s.title).toLowerCase().includes(query)) {
756
+ if (sessionSearchMatches(str(s.title), terms)) {
546
757
  results.push(redactRow({ ...s, match_type: 'title' }));
547
758
  continue;
548
759
  }
549
760
  if (!opts.content)
550
761
  continue;
551
- let sess;
552
- try {
553
- sess = this.store.get(str(s.session_id), { promote: false, cacheOnMiss: false });
762
+ const hit = this.contentMatch(str(s.session_id), terms, opts.depth);
763
+ if (hit)
764
+ results.push(redactRow({ ...s, ...hit }));
765
+ }
766
+ return { sessions: results, query, count: results.length, all_profiles: opts.allProfiles, active_profile: activeProfile };
767
+ }
768
+ /**
769
+ * TAL-308: the sidebar search. The candidates are exactly the `/api/sessions` rows for `params` (visibility, source,
770
+ * archived, projection and order), narrowed to `projectId` (`none`: rows without a project), then matched on the
771
+ * title, then the metadata the row shows (workspace path and name, model, provider, profile, source label), then message content.
772
+ */
773
+ sidebarSearch(q, params, opts) {
774
+ let rows = this.list(params).body.sessions;
775
+ if (opts.projectId === 'none')
776
+ rows = rows.filter((r) => !r.project_id);
777
+ else if (opts.projectId)
778
+ rows = rows.filter((r) => str(r.project_id) === opts.projectId);
779
+ const query = q.toLowerCase().trim();
780
+ const base = { all_profiles: params.allProfiles, active_profile: this.deps.activeProfile(), include_archived: params.includeArchived, sidebar_filtered: true };
781
+ if (!query)
782
+ return { ...base, sessions: rows };
783
+ const terms = sessionSearchTerms(query);
784
+ const results = [];
785
+ for (const row of rows) {
786
+ if (sessionSearchMatches(str(row.title), terms))
787
+ results.push({ ...row, match_type: 'title' });
788
+ else if (['workspace', 'workspace_name', 'model', 'model_provider', 'profile', 'source_label'].some((k) => sessionSearchMatches(str(row[k]), terms)))
789
+ results.push({ ...row, match_type: 'metadata' });
790
+ else if (opts.content) {
791
+ const hit = this.contentMatch(str(row.session_id), terms, opts.depth);
792
+ if (hit)
793
+ results.push({ ...row, ...hit });
554
794
  }
555
- catch {
795
+ }
796
+ return { ...base, sessions: results, query, count: results.length };
797
+ }
798
+ /** The first of a stored session's first `depth` messages (0: all) containing every term, with its redacted excerpt. */
799
+ contentMatch(sid, terms, depth) {
800
+ let sess;
801
+ try {
802
+ sess = this.store.get(sid, { promote: false, cacheOnMiss: false });
803
+ }
804
+ catch {
805
+ return null;
806
+ }
807
+ for (const m of depth ? sess.messages.slice(0, depth) : sess.messages) {
808
+ const c = sessionSearchMessageText(m);
809
+ if (!sessionSearchMatches(c, terms))
556
810
  continue;
557
- }
558
- const msgs = opts.depth ? sess.messages.slice(0, opts.depth) : sess.messages;
559
- for (const m of msgs) {
560
- const c = sessionSearchMessageText(m);
561
- if (c.toLowerCase().includes(query)) {
562
- const item = { ...s, match_type: 'content' };
563
- const preview = sessionSearchPreview(c, query);
564
- if (preview)
565
- item.match_preview = redactText(preview, redact);
566
- results.push(redactRow(item));
567
- break;
568
- }
569
- }
811
+ const item = { match_type: 'content' };
812
+ const preview = sessionSearchPreview(c, terms);
813
+ if (preview)
814
+ item.match_preview = redactText(preview, this.deps.redactEnabled());
815
+ return item;
570
816
  }
571
- return { sessions: results, query, count: results.length, all_profiles: opts.allProfiles, active_profile: activeProfile };
817
+ return null;
572
818
  }
573
819
  // ── status / usage ───────────────────────────────────────────────────────
574
820
  status(sid) {
@@ -618,17 +864,20 @@ export class SessionService {
618
864
  const [workspace] = this.deps.workspaces.resolveImplicitWithRecovery(value, (p) => this.deps.workspaces.profileDefaultWorkspaceForBoot(p), profile);
619
865
  return workspace;
620
866
  }
867
+ /** The server owns the names' normalization (TAL-631): trimmed, blanks dropped, and nothing left is the profile's defaults. */
621
868
  validateToolsetsShape(toolsets) {
622
869
  if (toolsets === null || toolsets === undefined)
623
870
  return null;
624
- if (!Array.isArray(toolsets) || !toolsets.length)
625
- throw new HttpFailure(400, 'toolsets must be a non-empty list or null');
626
- if (!toolsets.every((t) => typeof t === 'string' && t))
627
- throw new HttpFailure(400, 'each toolset must be a non-empty string');
628
- return toolsets;
871
+ if (!Array.isArray(toolsets))
872
+ throw new HttpFailure(400, 'toolsets must be a list or null');
873
+ if (!toolsets.every((t) => typeof t === 'string'))
874
+ throw new HttpFailure(400, 'each toolset must be a string');
875
+ const names = toolsets.map((t) => t.trim()).filter(Boolean);
876
+ return names.length ? names : null;
629
877
  }
630
878
  create(body, opts = {}) {
631
- const profile = (typeof body.profile === 'string' && body.profile) || null;
879
+ // Python `new_session`: a body without a profile takes the request's active profile (cookie first).
880
+ const profile = (typeof body.profile === 'string' && body.profile) || this.deps.activeProfile();
632
881
  let prevSessionId = typeof body.prev_session_id === 'string' && body.prev_session_id ? body.prev_session_id : null;
633
882
  if (prevSessionId && !this.sessionIdVisible(prevSessionId))
634
883
  prevSessionId = null;
@@ -819,6 +1068,7 @@ export class SessionService {
819
1068
  throw new HttpFailure(400, 'keep_count must be non-negative');
820
1069
  await this.store.withLock(sid, () => {
821
1070
  truncateSessionAtKeep(s, keep);
1071
+ this.markStateDbSeen(s, undefined, { boundary: true });
822
1072
  this.store.save(s);
823
1073
  });
824
1074
  this.deps.runtime.evictAgent(sid);
@@ -930,7 +1180,8 @@ export class SessionService {
930
1180
  if (streamState(live) !== streamBefore)
931
1181
  throw new HttpFailure(409, 'Session stream state changed during compression; please retry.');
932
1182
  // One state.db read serves the check, the kept display rows, and the boundary.
933
- const stateRows = this.stateDbRows(live);
1183
+ const read = this.stateDbRead(live);
1184
+ const stateRows = read.rows;
934
1185
  if (transcriptKey(live, sanitizeMessagesForApi(this.modelContext(live, stateRows))) !== historyKey)
935
1186
  throw new HttpFailure(409, 'Session was modified during compression; please retry.');
936
1187
  // Rows the sidecar returns without a timestamp take the newest one this compression saw, never the clock: the
@@ -960,6 +1211,8 @@ export class SessionService {
960
1211
  // #4836: an intentional-shrink boundary, so append-only state.db reconciliation does not replay compressed rows.
961
1212
  live.truncation_watermark = truncationWatermarkFor(compressed);
962
1213
  live.truncation_boundary = live.truncation_watermark;
1214
+ live.truncation_watermark_compressed = true;
1215
+ this.markStateDbSeen(live, read, { boundary: true });
963
1216
  live.last_prompt_tokens = result.after_tokens;
964
1217
  live.post_compression_context_tokens_estimate = result.after_tokens;
965
1218
  this.store.save(live);
@@ -1021,6 +1274,7 @@ export class SessionService {
1021
1274
  s.pending_user_source = null;
1022
1275
  s.clear_generation = hadMessages ? randomUUID().replace(/-/g, '') : null;
1023
1276
  applySessionTitleRename(s, 'Untitled');
1277
+ this.markStateDbSeen(s, undefined, { boundary: true });
1024
1278
  this.store.save(s);
1025
1279
  if (hadMessages) {
1026
1280
  try {
@@ -1029,7 +1283,7 @@ export class SessionService {
1029
1283
  catch { /* ignore */ }
1030
1284
  }
1031
1285
  });
1032
- this.deps.runtime.evictAgent(sid);
1286
+ this.deps.runtime.evictAgent(sid, true);
1033
1287
  return { ok: true, session: this.wireRow(s) };
1034
1288
  }
1035
1289
  async retry(sid) {
@@ -1040,11 +1294,20 @@ export class SessionService {
1040
1294
  const lastUser = findLastUserIndex(history);
1041
1295
  if (lastUser === null)
1042
1296
  return { error: 'No previous message to retry.' };
1043
- const lastUserText = extractText(history[lastUser]?.content);
1297
+ // What a client resends (TAL-515): the files that reached the model, which only ever got attachments with a path,
1298
+ // and the prompt as typed. The attached-files line is the server's only when such files exist; otherwise the user
1299
+ // typed it. Nothing to resend fails before the exchange is removed.
1300
+ const prompt = history[lastUser];
1301
+ const lastUserText = extractText(prompt.content);
1302
+ const lastUserAttachments = attachmentObjects(Array.isArray(prompt.attachments) ? prompt.attachments : []).filter((a) => isDict(a) && Boolean(str(a.path).trim()));
1303
+ const lastUserPrompt = stripWorkspacePrefix(lastUserAttachments.length ? stripAttachedFilesMarker(lastUserText) : lastUserText, true);
1304
+ if (!lastUserPrompt && !lastUserAttachments.length)
1305
+ return { error: 'The last message has nothing to resend.' };
1044
1306
  const removed = history.length - lastUser;
1045
1307
  shrinkTo(s, lastUser);
1308
+ this.markStateDbSeen(s, undefined, { boundary: true });
1046
1309
  this.store.save(s);
1047
- return { ok: true, last_user_text: lastUserText, removed_count: removed };
1310
+ return { ok: true, last_user_text: lastUserText, last_user_prompt: lastUserPrompt, last_user_attachments: lastUserAttachments, removed_count: removed };
1048
1311
  });
1049
1312
  }
1050
1313
  async undo(sid) {
@@ -1058,6 +1321,7 @@ export class SessionService {
1058
1321
  const removedText = extractText(history[lastUser]?.content);
1059
1322
  const removed = history.length - lastUser;
1060
1323
  shrinkTo(s, lastUser);
1324
+ this.markStateDbSeen(s, undefined, { boundary: true });
1061
1325
  this.store.save(s);
1062
1326
  const preview = removedText.length > 40 ? `${removedText.slice(0, 40)}...` : removedText;
1063
1327
  return { ok: true, removed_count: removed, removed_preview: preview };
@@ -1069,12 +1333,14 @@ export class SessionService {
1069
1333
  if (!session)
1070
1334
  throw new HttpFailure(404, 'Session not found');
1071
1335
  const now = this.deps.now();
1336
+ // A truncation cut names deleted state.db rows of the source; the copy's own state.db rows are all genuine (TAL-504).
1337
+ const watermark = session.truncation_watermark_compressed ? { truncation_watermark: session.truncation_watermark, truncation_boundary: session.truncation_boundary, truncation_watermark_compressed: true } : {};
1072
1338
  const copied = new Session({
1073
1339
  title: `${session.title || 'Untitled'} (copy)`, workspace: session.workspace, model: session.model, model_provider: session.model_provider,
1074
1340
  messages: copyJson(session.messages), tool_calls: copyJson(session.tool_calls), pinned: false, archived: false, project_id: session.project_id, profile: session.profile,
1075
1341
  input_tokens: session.input_tokens, output_tokens: session.output_tokens, estimated_cost: session.estimated_cost, cache_read_tokens: session.cache_read_tokens, cache_write_tokens: session.cache_write_tokens,
1076
1342
  personality: session.personality, enabled_toolsets: session.enabled_toolsets, context_length: session.context_length, threshold_tokens: session.threshold_tokens,
1077
- truncation_watermark: session.truncation_watermark, truncation_boundary: session.truncation_boundary, context_messages: copyJson(session.context_messages),
1343
+ ...watermark, context_messages: copyJson(session.context_messages),
1078
1344
  gateway_routing: copyJson(session.gateway_routing), gateway_routing_history: copyJson(session.gateway_routing_history), llm_title_generated: session.llm_title_generated,
1079
1345
  manual_title: session.manual_title, composer_draft: copyJson(session.composer_draft), context_engine: session.context_engine, context_engine_state: copyJson(session.context_engine_state),
1080
1346
  created_at: now, updated_at: now,
@@ -1131,6 +1397,19 @@ export class SessionService {
1131
1397
  }
1132
1398
  return { session_id: branch.session_id, title, parent_session_id: source.session_id };
1133
1399
  }
1400
+ /** The run that keeps `delete` answering 409, if any; stream status reports it so a draining client can wait on it (TAL-622). */
1401
+ activeRunBlocking(sid) {
1402
+ const live = this.deps.runtime.activeRunStream(sid);
1403
+ if (live)
1404
+ return live;
1405
+ try {
1406
+ const candidate = str(this.store.get(sid, { metadataOnly: true, promote: false, cacheOnMiss: false }).active_stream_id).trim();
1407
+ if (candidate && this.deps.runtime.activeStreamIds.has(candidate))
1408
+ return candidate;
1409
+ }
1410
+ catch { /* absent */ }
1411
+ return null;
1412
+ }
1134
1413
  async delete(sid) {
1135
1414
  if (!sid)
1136
1415
  throw new HttpFailure(400, 'session_id is required');
@@ -1163,25 +1442,20 @@ export class SessionService {
1163
1442
  return true;
1164
1443
  }
1165
1444
  catch { /* absent */ } const meta = this.lookupCliMeta(sid); return meta !== null && isMessagingSessionRecord(meta); })();
1166
- const blocking = () => {
1167
- const live = this.deps.runtime.activeRunStream(sid);
1168
- if (live)
1169
- return live;
1170
- try {
1171
- const snapshot = this.store.get(sid, { metadataOnly: true });
1172
- const candidate = str(snapshot.active_stream_id).trim();
1173
- if (candidate && this.deps.runtime.activeStreamIds.has(candidate))
1174
- return candidate;
1175
- }
1176
- catch { /* absent */ }
1177
- return null;
1178
- };
1179
- if (blocking())
1445
+ if (this.activeRunBlocking(sid))
1180
1446
  throw new HttpFailure(409, 'Session has an active run; stop it before deleting');
1181
1447
  try {
1182
1448
  await this.store.withLock(sid, () => {
1183
- if (blocking())
1449
+ if (this.activeRunBlocking(sid))
1184
1450
  throw new HttpFailure(409, 'Session has an active run; stop it before deleting');
1451
+ // A public share outlives its session file, and revokeShare needs the session, so revoke it first.
1452
+ try {
1453
+ this.deps.shares.revoke(this.store.get(sid, { metadataOnly: true }));
1454
+ }
1455
+ catch (error) {
1456
+ if (!(error instanceof SessionNotFound))
1457
+ throw error;
1458
+ }
1185
1459
  if (!this.store.deleteFiles(sid))
1186
1460
  throw new HttpFailure(500, 'Failed to delete session data');
1187
1461
  }, { timeoutMs: 5000 });
@@ -1191,7 +1465,7 @@ export class SessionService {
1191
1465
  throw new HttpFailure(503, 'Session busy, try again');
1192
1466
  throw error;
1193
1467
  }
1194
- this.deps.runtime.evictAgent(sid);
1468
+ this.deps.runtime.evictAgent(sid, true);
1195
1469
  try {
1196
1470
  rmSync(this.deps.attachmentDir(sid), { recursive: true, force: true });
1197
1471
  }
@@ -1206,11 +1480,15 @@ export class SessionService {
1206
1480
  // messaging channel's memory is never erased from the WebUI; the actual outcome is reported.
1207
1481
  let stateDbCleanupFailed = false;
1208
1482
  if (!isMessaging) {
1209
- try {
1210
- stateDbCleanupFailed = !(await this.deps.runtime.deleteCliSession(eventProfile, sid));
1211
- }
1212
- catch {
1213
- stateDbCleanupFailed = true;
1483
+ // TAL-529: the compression continuations go too, tip first; the Agent's delete keeps and detaches them otherwise.
1484
+ for (const id of stateDbCompressionLineage(this.stateDbPath(eventProfile), sid).reverse()) {
1485
+ try {
1486
+ if (!(await this.deps.runtime.deleteCliSession(eventProfile, id)))
1487
+ stateDbCleanupFailed = true;
1488
+ }
1489
+ catch {
1490
+ stateDbCleanupFailed = true;
1491
+ }
1214
1492
  }
1215
1493
  }
1216
1494
  this.publish('session_delete', eventProfile);
@@ -1226,6 +1504,13 @@ export class SessionService {
1226
1504
  const shouldDelete = zeroOnly ? s.messages.length === 0 : s.title === 'Untitled' && s.messages.length === 0;
1227
1505
  if (!shouldDelete)
1228
1506
  continue;
1507
+ // A cleared session keeps its share; keep the session (and its revoke route) if the share cannot be revoked.
1508
+ try {
1509
+ this.deps.shares.revoke(s);
1510
+ }
1511
+ catch {
1512
+ continue;
1513
+ }
1229
1514
  this.store.sessions.delete(s.session_id);
1230
1515
  rmSync(this.store.pathFor(s.session_id), { force: true });
1231
1516
  cleaned += 1;
@@ -1426,39 +1711,54 @@ export class SessionService {
1426
1711
  return { ok: true, ...placement };
1427
1712
  });
1428
1713
  }
1429
- readAnchorScene(query) {
1430
- const sid = str(query.session_id).trim();
1431
- const messageRef = normalizeAnchorSceneMessageRef(query.message_ref);
1432
- const messageIndex = anchorSceneIntOrNull(query.message_index);
1433
- if (!sid || (!messageRef && messageIndex === null))
1434
- throw new HttpFailure(400, 'session_id and message_ref or message_index are required');
1435
- let session;
1436
- let transcript;
1714
+ /** The session and the full transcript its detail is built from; a state.db-only session gives its synthesized one. */
1715
+ fullTranscript(sid) {
1437
1716
  try {
1438
- session = this.store.get(sid);
1717
+ let session = this.store.get(sid);
1439
1718
  if (session.loadedMetadataOnly)
1440
1719
  session = this.store.load(sid) ?? session;
1441
1720
  if (!this.visibleToActiveProfile(session.profile))
1442
1721
  throw new HttpFailure(404, 'Session not found');
1443
- transcript = this.mergedTranscript(session);
1722
+ return { session, transcript: this.mergedTranscript(session), stored: true };
1444
1723
  }
1445
1724
  catch (error) {
1446
1725
  if (error instanceof HttpFailure)
1447
1726
  throw error;
1448
- // A state.db-only session pages the same synthesized transcript its detail was built from.
1449
- session = this.foreignSession(sid).synth;
1450
- transcript = session.messages;
1727
+ const session = this.foreignSession(sid).synth;
1728
+ return { session, transcript: session.messages, stored: false };
1451
1729
  }
1730
+ }
1731
+ readAnchorScene(query) {
1732
+ const sid = str(query.session_id).trim();
1733
+ const messageRef = normalizeAnchorSceneMessageRef(query.message_ref);
1734
+ const messageIndex = anchorSceneIntOrNull(query.message_index);
1735
+ if (!sid || (!messageRef && messageIndex === null))
1736
+ throw new HttpFailure(400, 'session_id and message_ref or message_index are required');
1737
+ const { session, transcript, stored } = this.fullTranscript(sid);
1452
1738
  const result = readAnchorSceneRows(session, { messageRef, messageIndex, before: anchorSceneIntOrNull(query.before), limit: anchorSceneIntOrNull(query.limit) }, withToolCallOutcomes(withTurnIds(transcript), session.tool_calls, session.active_stream_id));
1453
1739
  if (!result)
1454
1740
  throw new HttpFailure(404, 'Anchor activity scene not found');
1455
1741
  // Paged rows come from the raw transcript, so they take the same credential redaction as the detail's preview.
1456
1742
  const enabled = this.deps.redactEnabled();
1457
1743
  const redacted = redactValue(result, enabled);
1458
- return { ...redacted, rows: withSceneToolDisplay(result.rows, redacted.rows, enabled) };
1744
+ const rows = withSceneToolDisplay(result.rows, redacted.rows, enabled);
1745
+ // `/api/media` serves stored sessions only, so a state.db-only session's rows stay as written, as its detail does.
1746
+ return { ...redacted, rows: stored ? withSceneRowMedia(rows, this.mediaProjector(session)) : rows };
1747
+ }
1748
+ /** TAL-331: one tool call's whole result, which a limited response clipped (`result_truncated`), redacted like the detail. */
1749
+ readToolResult(query) {
1750
+ const sid = query.session_id.trim();
1751
+ const id = query.tool_call_id.trim();
1752
+ if (!sid || !id)
1753
+ throw new HttpFailure(400, 'session_id and tool_call_id are required');
1754
+ const full = fullToolResult(this.fullTranscript(sid).transcript, id);
1755
+ if (full === null)
1756
+ throw new HttpFailure(404, 'Tool result not found');
1757
+ return { tool_call_id: id, ...redactValue(full, this.deps.redactEnabled()) };
1459
1758
  }
1460
1759
  // ── shares ───────────────────────────────────────────────────────────────
1461
1760
  createShare(sid) {
1761
+ this.rejectClaudeCode(sid, 'shared');
1462
1762
  let s = this.get404(sid);
1463
1763
  if (!this.visibleToActiveProfile(s.profile))
1464
1764
  throw new HttpFailure(404, 'Session not found');
@@ -1477,7 +1777,7 @@ export class SessionService {
1477
1777
  this.publish('session_share_create', s.profile, sid);
1478
1778
  return {
1479
1779
  ok: true,
1480
- 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 },
1780
+ 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 },
1481
1781
  session: this.publicSession(s),
1482
1782
  };
1483
1783
  }
@@ -1544,6 +1844,44 @@ function findLastUserIndex(history) {
1544
1844
  export function sanitizePaths(error) {
1545
1845
  return str(error?.message ?? error).replace(/(?:(?:\/[a-zA-Z0-9_.-]+)+|(?:[A-Z]:\\[^\s]+))/g, '<path>');
1546
1846
  }
1847
+ /** 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). */
1848
+ function runStateDbRows(rows, prompt) {
1849
+ const at = rows.findIndex((m) => looksLikeCurrentUserTurn(m, prompt));
1850
+ if (at < 0)
1851
+ return null;
1852
+ const toolResult = (m) => Array.isArray(m.content) && m.content.some((part) => isDict(part) && part.type === 'tool_result');
1853
+ const end = rows.findIndex((m, i) => i > at && m.role === 'user' && !toolResult(m) && agentSteerText(m) === null);
1854
+ return rows.slice(at, end < 0 ? undefined : end);
1855
+ }
1856
+ /** TAL-493: a row's full-text identity; one with no text (reasoning or media only) by its raw role, content, and reasoning. */
1857
+ function settledIdentity(m) {
1858
+ return messageIdentity(m, Infinity) ?? JSON.stringify([m.role ?? null, m.content ?? null, reasoningFieldsText(m)]);
1859
+ }
1860
+ /** `rows` minus one occurrence of each row of `base`, by `settledIdentity`. */
1861
+ function withoutRows(rows, base) {
1862
+ const left = new Map();
1863
+ for (const m of base)
1864
+ left.set(settledIdentity(m), (left.get(settledIdentity(m)) ?? 0) + 1);
1865
+ return rows.filter((m) => { const key = settledIdentity(m); const n = left.get(key) ?? 0; if (n > 0)
1866
+ left.set(key, n - 1); return n === 0; });
1867
+ }
1868
+ /**
1869
+ * TAL-493: what the state.db marker was recorded under: the boundary fields every boundary writer sets (an older release
1870
+ * moves them without the marker) and the role, timestamp, and database file identity of the row the marker names (a
1871
+ * recreated state.db, whose ids start over, has no such row).
1872
+ */
1873
+ function stateDbMarkKey(s, stateRows, seenId) {
1874
+ const anchor = seenId === null ? undefined : stateRows.find((m) => m._state_db_row_id === seenId);
1875
+ 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]);
1876
+ }
1877
+ /**
1878
+ * The recorded state.db marker while what it was recorded under still holds and this version wrote the session last (an
1879
+ * older release can apply a boundary that changes no field), else null (the timestamp rules apply).
1880
+ */
1881
+ function currentStateDbSeenId(s, stateRows) {
1882
+ const seenId = s.state_db_seen_id;
1883
+ return seenId !== null && s.state_db_seen_stamp === s.updated_at && s.state_db_seen_boundary === stateDbMarkKey(s, stateRows, seenId) ? seenId : null;
1884
+ }
1547
1885
  function truncationWatermarkFor(messages) {
1548
1886
  if (!messages.length)
1549
1887
  return 0.0;
@@ -1557,17 +1895,142 @@ function stampIntentionalShrink(session, oldCount, newCount) {
1557
1895
  session.intentional_shrink_generation = randomUUID().replace(/-/g, '');
1558
1896
  return true;
1559
1897
  }
1560
- /** Align model context with the display prefix (Python `truncate_context_for_display_keep`, simplified to the row-count contract). */
1898
+ /** Python truthiness for the JSON values a transcript row carries. */
1899
+ function pyTruthy(v) {
1900
+ if (Array.isArray(v))
1901
+ return v.length > 0;
1902
+ if (isDict(v))
1903
+ return Object.keys(v).length > 0;
1904
+ return Boolean(v);
1905
+ }
1906
+ /** JSON with sorted object keys (Python `json.dumps(..., sort_keys=True)`). */
1907
+ function sortedJson(v) {
1908
+ return JSON.stringify(v, (_key, value) => (isDict(value) ? Object.fromEntries(Object.keys(value).sort().map((k) => [k, value[k]])) : value)) ?? '';
1909
+ }
1910
+ /** Equality key for a row `id`/`timestamp`; `null` when the value is absent. Numbers compare by value (`2 == 2.0`). */
1911
+ function identityKey(v) {
1912
+ if (v === null || v === undefined)
1913
+ return null;
1914
+ return typeof v === 'string' ? `s${v}` : typeof v === 'number' ? `n${String(v)}` : `j${sortedJson(v)}`;
1915
+ }
1916
+ function rowSignature(row) {
1917
+ if (!isDict(row))
1918
+ return null;
1919
+ const field = (v) => (!pyTruthy(v) ? '' : typeof v === 'string' ? v : sortedJson(v));
1920
+ return JSON.stringify([field(row.role), field(row.content), field(row.tool_call_id), field(row.tool_use_id), field(row.tool_name || row.name), field(row.tool_calls)]);
1921
+ }
1922
+ /** First index in sorted `positions` whose value is `>= start`. */
1923
+ function lowerBound(positions, start) {
1924
+ let lo = 0;
1925
+ let hi = positions.length;
1926
+ while (lo < hi) {
1927
+ const mid = (lo + hi) >> 1;
1928
+ if (positions[mid] < start)
1929
+ lo = mid + 1;
1930
+ else
1931
+ hi = mid;
1932
+ }
1933
+ return lo;
1934
+ }
1935
+ /**
1936
+ * Map each display row to the context row it represents, scanning forward. An `id` match wins; otherwise the role+content
1937
+ * signature must match, and a shared `timestamp` makes it exact. A signature match where an identity is missing is weak:
1938
+ * one weak candidate counts as a match, a second one before any exact match leaves the row ambiguous.
1939
+ */
1940
+ function alignDisplayToContext(ctx, full) {
1941
+ const index = () => new Map();
1942
+ const push = (map, key, idx) => {
1943
+ const list = map.get(key);
1944
+ if (list)
1945
+ list.push(idx);
1946
+ else
1947
+ map.set(key, [idx]);
1948
+ };
1949
+ const first = (list, start) => list?.[lowerBound(list, start)];
1950
+ const byId = index(), bySigTs = index(), bySigTsNoId = index();
1951
+ const weakAny = index(), weakNoId = index(), weakNoTs = index(), weakNoIdNoTs = index();
1952
+ ctx.forEach((row, idx) => {
1953
+ const sig = rowSignature(row);
1954
+ if (sig === null)
1955
+ return;
1956
+ const id = identityKey(row.id);
1957
+ const ts = identityKey(row.timestamp);
1958
+ push(weakAny, sig, idx);
1959
+ if (id === null)
1960
+ push(weakNoId, sig, idx);
1961
+ if (ts === null)
1962
+ push(weakNoTs, sig, idx);
1963
+ if (id === null && ts === null)
1964
+ push(weakNoIdNoTs, sig, idx);
1965
+ if (id !== null)
1966
+ push(byId, id, idx);
1967
+ if (ts !== null) {
1968
+ push(bySigTs, `${sig}\0${ts}`, idx);
1969
+ if (id === null)
1970
+ push(bySigTsNoId, `${sig}\0${ts}`, idx);
1971
+ }
1972
+ });
1973
+ const matches = [];
1974
+ const ambiguous = [];
1975
+ let next = 0;
1976
+ for (const message of full) {
1977
+ let match = null;
1978
+ let ambiguousAt = null;
1979
+ const sig = rowSignature(message);
1980
+ if (sig !== null) {
1981
+ const id = identityKey(message.id);
1982
+ const ts = identityKey(message.timestamp);
1983
+ const exact = [id !== null ? first(byId.get(id), next) : undefined, ts !== null ? first((id !== null ? bySigTsNoId : bySigTs).get(`${sig}\0${ts}`), next) : undefined]
1984
+ .filter((idx) => idx !== undefined);
1985
+ const exactIdx = exact.length ? Math.min(...exact) : null;
1986
+ const weakList = (id !== null ? (ts !== null ? weakNoIdNoTs : weakNoId) : ts !== null ? weakNoTs : weakAny).get(sig) ?? [];
1987
+ const w = lowerBound(weakList, next);
1988
+ const [firstWeak, secondWeak] = [weakList[w], weakList[w + 1]];
1989
+ if (secondWeak !== undefined && (exactIdx === null || secondWeak < exactIdx))
1990
+ ambiguousAt = firstWeak;
1991
+ else
1992
+ match = exactIdx ?? firstWeak ?? null;
1993
+ }
1994
+ matches.push(match);
1995
+ ambiguous.push(ambiguousAt);
1996
+ if (match !== null)
1997
+ next = match + 1;
1998
+ }
1999
+ return { matches, ambiguous };
2000
+ }
2001
+ /** Align model context with the display prefix `full[:keep]` (Python `truncate_context_for_display_keep`, #5096/#5563). */
1561
2002
  export function truncateContextForDisplayKeep(context, full, keep) {
1562
2003
  if (keep <= 0)
1563
2004
  return [];
1564
- const rows = context ?? [];
1565
- if (!rows.length)
2005
+ const ctx = context ?? [];
2006
+ if (!ctx.length || !full.length)
1566
2007
  return [];
1567
- if (keep >= full.length)
1568
- return [...rows];
1569
- // ponytail: context rows map 1:1 onto display rows for WebUI-authored transcripts; fuzzy alignment stays in Python until compression lands.
1570
- return rows.slice(0, Math.min(rows.length, keep));
2008
+ if (keep < full.length) {
2009
+ const { matches, ambiguous } = alignDisplayToContext(ctx, full);
2010
+ const lastKept = matches[keep - 1];
2011
+ const firstUnkept = matches[keep];
2012
+ const boundary = full[keep - 1];
2013
+ const keptUser = isDict(boundary) && boundary.role === 'user';
2014
+ // Cut at the first unkept display row; a kept user turn ends at its own row so unkept tool rows after it drop.
2015
+ if (firstUnkept !== null)
2016
+ return lastKept !== null && keptUser ? ctx.slice(0, lastKept + 1) : ctx.slice(0, firstUnkept);
2017
+ if (lastKept !== null) {
2018
+ const ambiguousUnkept = ambiguous[keep];
2019
+ return ambiguousUnkept !== null && isDict(boundary) && !keptUser ? ctx.slice(0, ambiguousUnkept) : ctx.slice(0, lastKept + 1);
2020
+ }
2021
+ // Both boundary rows unresolved in a shorter (compressed) context: cut past the last kept display row that resolved,
2022
+ // accepting a weak match. Under-keeping beats slicing at the raw display index.
2023
+ if (ctx.length < full.length) {
2024
+ for (let i = keep - 1; i >= 0; i -= 1) {
2025
+ const resolved = matches[i] ?? ambiguous[i];
2026
+ if (resolved !== null && resolved !== undefined)
2027
+ return ctx.slice(0, resolved + 1);
2028
+ }
2029
+ }
2030
+ }
2031
+ // Unalignable (or nothing cut): keep the compaction prefix a longer context carries, then `keep` rows (#5096).
2032
+ const prefixLen = Math.max(0, ctx.length - full.length);
2033
+ return ctx.slice(0, prefixLen + keep);
1571
2034
  }
1572
2035
  export function truncateSessionAtKeep(session, keep) {
1573
2036
  const full = [...session.messages];
@@ -1578,6 +2041,7 @@ export function truncateSessionAtKeep(session, keep) {
1578
2041
  session.context_messages = truncateContextForDisplayKeep(session.context_messages, full, keep);
1579
2042
  session.truncation_watermark = truncationWatermarkFor(session.messages);
1580
2043
  session.truncation_boundary = session.truncation_watermark;
2044
+ session.truncation_watermark_compressed = false;
1581
2045
  return [oldMsgCount, oldCtxCount];
1582
2046
  }
1583
2047
  function shrinkTo(session, lastUserIdx) {
@@ -1586,6 +2050,7 @@ function shrinkTo(session, lastUserIdx) {
1586
2050
  stampIntentionalShrink(session, history.length, session.messages.length);
1587
2051
  session.truncation_watermark = truncationWatermarkFor(session.messages);
1588
2052
  session.truncation_boundary = session.truncation_watermark;
2053
+ session.truncation_watermark_compressed = false;
1589
2054
  if (session.context_messages.length) {
1590
2055
  const ctxLast = findLastUserIndex(session.context_messages);
1591
2056
  if (ctxLast !== null)