@maudecode/talaria-web 1.14.0 → 1.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (348) hide show
  1. package/_release.json +4 -4
  2. package/dist/api/automation-router.d.ts +123 -7
  3. package/dist/api/automation-router.js +5 -0
  4. package/dist/api/automation-router.js.map +1 -1
  5. package/dist/api/chat-router.d.ts +728 -55
  6. package/dist/api/chat-router.js +122 -126
  7. package/dist/api/chat-router.js.map +1 -1
  8. package/dist/api/git-router.js +44 -32
  9. package/dist/api/git-router.js.map +1 -1
  10. package/dist/api/raw-routes.js +5 -58
  11. package/dist/api/raw-routes.js.map +1 -1
  12. package/dist/api/router.d.ts +6 -3
  13. package/dist/api/router.js +10 -8
  14. package/dist/api/router.js.map +1 -1
  15. package/dist/api/sessions-router.d.ts +2583 -297
  16. package/dist/api/sessions-router.js +92 -47
  17. package/dist/api/sessions-router.js.map +1 -1
  18. package/dist/api/settings-router.d.ts +188 -17
  19. package/dist/api/settings-router.js +75 -26
  20. package/dist/api/settings-router.js.map +1 -1
  21. package/dist/api/sse-routes.js +1 -1
  22. package/dist/api/sse-routes.js.map +1 -1
  23. package/dist/api/tools-router.d.ts +13 -1
  24. package/dist/api/tools-router.js +10 -8
  25. package/dist/api/tools-router.js.map +1 -1
  26. package/dist/app.d.ts +3620 -343
  27. package/dist/auth/gate.js +8 -4
  28. package/dist/auth/gate.js.map +1 -1
  29. package/dist/auth/oidc.d.ts +6 -2
  30. package/dist/auth/oidc.js +16 -11
  31. package/dist/auth/oidc.js.map +1 -1
  32. package/dist/auth/store.d.ts +14 -2
  33. package/dist/auth/store.js +26 -1
  34. package/dist/auth/store.js.map +1 -1
  35. package/dist/cli/dotenv.d.ts +1 -1
  36. package/dist/cli/dotenv.js +36 -3
  37. package/dist/cli/dotenv.js.map +1 -1
  38. package/dist/config/agent-config.js +20 -15
  39. package/dist/config/agent-config.js.map +1 -1
  40. package/dist/http/addresses.d.ts +6 -0
  41. package/dist/http/addresses.js +26 -0
  42. package/dist/http/addresses.js.map +1 -1
  43. package/dist/http/context.d.ts +7 -5
  44. package/dist/http/context.js.map +1 -1
  45. package/dist/links.d.ts +26 -0
  46. package/dist/links.js +60 -0
  47. package/dist/links.js.map +1 -0
  48. package/dist/onboarding.d.ts +1 -1
  49. package/dist/onboarding.js +28 -8
  50. package/dist/onboarding.js.map +1 -1
  51. package/dist/profiles/profiles.js +1 -1
  52. package/dist/profiles/profiles.js.map +1 -1
  53. package/dist/providers/catalog.d.ts +85 -1
  54. package/dist/providers/catalog.js +280 -43
  55. package/dist/providers/catalog.js.map +1 -1
  56. package/dist/providers/tables.d.ts +3 -1
  57. package/dist/providers/tables.js +9 -4
  58. package/dist/providers/tables.js.map +1 -1
  59. package/dist/redact.js +493 -13
  60. package/dist/redact.js.map +1 -1
  61. package/dist/runtime.js +104 -42
  62. package/dist/runtime.js.map +1 -1
  63. package/dist/sessions/anchor.d.ts +22 -3
  64. package/dist/sessions/anchor.js +87 -42
  65. package/dist/sessions/anchor.js.map +1 -1
  66. package/dist/sessions/background-tasks.d.ts +140 -0
  67. package/dist/sessions/background-tasks.js +352 -0
  68. package/dist/sessions/background-tasks.js.map +1 -0
  69. package/dist/sessions/background-updates.d.ts +56 -0
  70. package/dist/sessions/background-updates.js +152 -0
  71. package/dist/sessions/background-updates.js.map +1 -0
  72. package/dist/sessions/cli-sessions.d.ts +6 -0
  73. package/dist/sessions/cli-sessions.js +20 -7
  74. package/dist/sessions/cli-sessions.js.map +1 -1
  75. package/dist/sessions/completions.d.ts +17 -1
  76. package/dist/sessions/completions.js +84 -13
  77. package/dist/sessions/completions.js.map +1 -1
  78. package/dist/sessions/compress.d.ts +33 -0
  79. package/dist/sessions/compress.js +66 -0
  80. package/dist/sessions/compress.js.map +1 -0
  81. package/dist/sessions/list.d.ts +22 -4
  82. package/dist/sessions/list.js +113 -33
  83. package/dist/sessions/list.js.map +1 -1
  84. package/dist/sessions/merge.d.ts +81 -7
  85. package/dist/sessions/merge.js +391 -26
  86. package/dist/sessions/merge.js.map +1 -1
  87. package/dist/sessions/pending.d.ts +5 -0
  88. package/dist/sessions/pending.js +30 -10
  89. package/dist/sessions/pending.js.map +1 -1
  90. package/dist/sessions/relay.d.ts +7 -1
  91. package/dist/sessions/relay.js +17 -4
  92. package/dist/sessions/relay.js.map +1 -1
  93. package/dist/sessions/service.d.ts +118 -6
  94. package/dist/sessions/service.js +564 -65
  95. package/dist/sessions/service.js.map +1 -1
  96. package/dist/sessions/session.d.ts +22 -1
  97. package/dist/sessions/session.js +29 -2
  98. package/dist/sessions/session.js.map +1 -1
  99. package/dist/sessions/shares.js +11 -2
  100. package/dist/sessions/shares.js.map +1 -1
  101. package/dist/sessions/source-kind.d.ts +22 -0
  102. package/dist/sessions/source-kind.js +54 -0
  103. package/dist/sessions/source-kind.js.map +1 -0
  104. package/dist/sessions/state-db.d.ts +10 -1
  105. package/dist/sessions/state-db.js +48 -17
  106. package/dist/sessions/state-db.js.map +1 -1
  107. package/dist/sessions/store.d.ts +2 -0
  108. package/dist/sessions/store.js +24 -5
  109. package/dist/sessions/store.js.map +1 -1
  110. package/dist/sessions/streams.d.ts +3 -0
  111. package/dist/sessions/streams.js +0 -5
  112. package/dist/sessions/streams.js.map +1 -1
  113. package/dist/sessions/titles.d.ts +4 -0
  114. package/dist/sessions/titles.js +16 -0
  115. package/dist/sessions/titles.js.map +1 -1
  116. package/dist/sessions/turn-context.d.ts +1 -0
  117. package/dist/sessions/turn-context.js +3 -0
  118. package/dist/sessions/turn-context.js.map +1 -1
  119. package/dist/sessions/turn.d.ts +85 -7
  120. package/dist/sessions/turn.js +518 -170
  121. package/dist/sessions/turn.js.map +1 -1
  122. package/dist/settings.d.ts +2 -0
  123. package/dist/settings.js +13 -0
  124. package/dist/settings.js.map +1 -1
  125. package/dist/sidecar/fake.js +2 -0
  126. package/dist/sidecar/fake.js.map +1 -1
  127. package/dist/tools/commands.d.ts +15 -0
  128. package/dist/tools/commands.js +52 -0
  129. package/dist/tools/commands.js.map +1 -0
  130. package/dist/tools/crons.d.ts +38 -4
  131. package/dist/tools/crons.js +323 -36
  132. package/dist/tools/crons.js.map +1 -1
  133. package/dist/tools/mcp.js +5 -1
  134. package/dist/tools/mcp.js.map +1 -1
  135. package/dist/tools/update-notifications.d.ts +1 -1
  136. package/dist/tools/update-notifications.js +1 -1
  137. package/dist/tools/update-notifications.js.map +1 -1
  138. package/dist/tools/updates.d.ts +8 -3
  139. package/dist/tools/updates.js +133 -34
  140. package/dist/tools/updates.js.map +1 -1
  141. package/dist/workspace/git.d.ts +57 -10
  142. package/dist/workspace/git.js +302 -195
  143. package/dist/workspace/git.js.map +1 -1
  144. package/dist/workspace/media-refs.d.ts +13 -0
  145. package/dist/workspace/media-refs.js +165 -0
  146. package/dist/workspace/media-refs.js.map +1 -0
  147. package/dist/workspace/media.d.ts +19 -0
  148. package/dist/workspace/media.js +61 -2
  149. package/dist/workspace/media.js.map +1 -1
  150. package/dist/workspace/workspaces.d.ts +7 -1
  151. package/dist/workspace/workspaces.js +30 -21
  152. package/dist/workspace/workspaces.js.map +1 -1
  153. package/dist/workspace/worktrees.d.ts +2 -4
  154. package/dist/workspace/worktrees.js +28 -33
  155. package/dist/workspace/worktrees.js.map +1 -1
  156. package/package.json +4 -2
  157. package/sidecar/scripts/replay_sidecar.py +3 -1
  158. package/sidecar/talaria_sidecar/__init__.py +1 -1
  159. package/sidecar/talaria_sidecar/home.py +59 -2
  160. package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
  161. package/sidecar/talaria_sidecar/methods/aux.py +19 -3
  162. package/sidecar/talaria_sidecar/methods/chat.py +360 -27
  163. package/sidecar/talaria_sidecar/methods/commands.py +46 -31
  164. package/sidecar/talaria_sidecar/methods/config.py +14 -10
  165. package/sidecar/talaria_sidecar/methods/cron.py +22 -14
  166. package/sidecar/talaria_sidecar/methods/oauth.py +439 -0
  167. package/sidecar/talaria_sidecar/methods/process.py +318 -0
  168. package/sidecar/talaria_sidecar/methods/profiles.py +5 -4
  169. package/sidecar/talaria_sidecar/methods/providers.py +161 -1
  170. package/sidecar/talaria_sidecar/methods/runtime.py +2 -0
  171. package/sidecar/talaria_sidecar/methods/text.py +1 -20
  172. package/sidecar/talaria_sidecar/methods/usage.py +7 -1
  173. package/static/dist/FILES.txt +124 -119
  174. package/static/dist/assets/AppShell-ef3bhu32.js +1 -0
  175. package/static/dist/assets/Button-nx37f4u1.js +1 -0
  176. package/static/dist/assets/ChatPage-cdcpykcg.js +25 -0
  177. package/static/dist/assets/Dialog-mec42hvb.js +1 -0
  178. package/static/dist/assets/HubRoute-ecb65zpr.js +2 -0
  179. package/static/dist/assets/{Markdown-emii55o0.js → Markdown-jqrx5es0.js} +125 -125
  180. package/static/dist/assets/RightPanel-d1lgvk4s.js +1 -0
  181. package/static/dist/assets/Select-h4ivl9qf.js +1 -0
  182. package/static/dist/assets/SessionListPanel-ewib046b.js +1 -0
  183. package/static/dist/assets/SettingsLayout-lx654cup.js +1 -0
  184. package/static/dist/assets/States-ehsb6lzo.js +1 -0
  185. package/static/dist/assets/Toaster-mch1pa3t.js +1 -0
  186. package/static/dist/assets/_app-m8d961ef.js +1 -0
  187. package/static/dist/assets/_app.ext._extensionId-mp33hbzi.js +1 -0
  188. package/static/dist/assets/_app.index-dk6zp7e0.js +1 -0
  189. package/static/dist/assets/_app.insights-jt2fk4i1.js +1 -0
  190. package/static/dist/assets/_app.kanban-if4pubeu.js +1 -0
  191. package/static/dist/assets/_app.logs-mfaw9m6g.js +1 -0
  192. package/static/dist/assets/_app.memory-h3eaarb6.js +1 -0
  193. package/static/dist/assets/_app.profiles-ienktovs.js +1 -0
  194. package/static/dist/assets/{_app.session._sessionId-m6nx9g8t.js → _app.session._sessionId-hh27wwu2.js} +1 -1
  195. package/static/dist/assets/_app.settings-dpoe0bzf.js +1 -0
  196. package/static/dist/assets/_app.settings._section-l468ynag.js +3 -0
  197. package/static/dist/assets/_app.skills-kcbncc8i.js +1 -0
  198. package/static/dist/assets/_app.tasks-occdrqhw.js +2 -0
  199. package/static/dist/assets/_app.todos-nnke2rcc.js +1 -0
  200. package/static/dist/assets/_app.workspaces-cdlatvwj.js +1 -0
  201. package/static/dist/assets/{abnfDiagram-VCTEODGH-kejgxpil.js → abnfDiagram-VCTEODGH-oity9y4o.js} +1 -1
  202. package/static/dist/assets/architecture-7GRP2DOG-hj11333z.js +1 -0
  203. package/static/dist/assets/architectureDiagram-5GKGNRK7-ceaepnyr.js +36 -0
  204. package/static/dist/assets/back-eu64w89w.js +1 -0
  205. package/static/dist/assets/{blockDiagram-I7D4REHJ-l1466a7j.js → blockDiagram-I7D4REHJ-n79st4pj.js} +1 -1
  206. package/static/dist/assets/{c4Diagram-7LVT6UL2-gg3aq3sr.js → c4Diagram-7LVT6UL2-mi73akzk.js} +1 -1
  207. package/static/dist/assets/cancel-mzvkdjg8.js +1 -0
  208. package/static/dist/assets/channel-mbvmf2vv.js +1 -0
  209. package/static/dist/assets/{chunk-4HAMMTFA-oalgz5l8.js → chunk-4HAMMTFA-laat0g1q.js} +1 -1
  210. package/static/dist/assets/{chunk-75Z2AOVW-babmzhdg.js → chunk-75Z2AOVW-0psy1tr2.js} +1 -1
  211. package/static/dist/assets/{chunk-DU6HZSFF-lagu350c.js → chunk-DU6HZSFF-lw8uox8x.js} +3 -3
  212. package/static/dist/assets/{chunk-F27PBJKO-gjh2tqif.js → chunk-F27PBJKO-erassib3.js} +1 -1
  213. package/static/dist/assets/{chunk-GMAD6QVW-ipdgja5c.js → chunk-GMAD6QVW-fz7rx619.js} +1 -1
  214. package/static/dist/assets/{chunk-GVQU2GXP-j8wkn1at.js → chunk-GVQU2GXP-gdb0epb2.js} +1 -1
  215. package/static/dist/assets/{chunk-IMKFNOWR-k00laqyk.js → chunk-IMKFNOWR-ea8t26qw.js} +1 -1
  216. package/static/dist/assets/{chunk-L3NEJ4N5-fju6xqj4.js → chunk-L3NEJ4N5-mb1gu9kz.js} +1 -1
  217. package/static/dist/assets/{chunk-OSK3NFVY-bi6widqt.js → chunk-OSK3NFVY-eovmccvy.js} +1 -1
  218. package/static/dist/assets/{chunk-P2QGCYS3-czr4tebv.js → chunk-P2QGCYS3-bo55zjo5.js} +1 -1
  219. package/static/dist/assets/{chunk-POPQ4Y6H-ktdhn67w.js → chunk-POPQ4Y6H-ja7ohe4h.js} +1 -1
  220. package/static/dist/assets/{chunk-PWAF6VOD-ii4dsu0d.js → chunk-PWAF6VOD-e30g9nus.js} +1 -1
  221. package/static/dist/assets/{chunk-SHT3W25Y-fpwk3xxp.js → chunk-SHT3W25Y-gbb798f7.js} +3 -3
  222. package/static/dist/assets/{chunk-SVP7TREG-k0jrvhei.js → chunk-SVP7TREG-h2iw8urm.js} +1 -1
  223. package/static/dist/assets/{chunk-TICWLB2K-nkqv9ot0.js → chunk-TICWLB2K-fd2cottz.js} +1 -1
  224. package/static/dist/assets/classDiagram-ZZMXUADV-o8r05k0n.js +1 -0
  225. package/static/dist/assets/classDiagram-v2-VYDZK3BY-o8r05k0n.js +1 -0
  226. package/static/dist/assets/{copied-gpchexqu.js → copied-h65fc3cx.js} +1 -1
  227. package/static/dist/assets/copy-e6977uwq.js +1 -0
  228. package/static/dist/assets/{cynefin-OW5HDTMX-lqxt8rs3.js → cynefin-OW5HDTMX-lbltyl5j.js} +1 -1
  229. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-j9eqovf5.js → cynefinDiagram-5FMLGOSQ-lxhsqq4a.js} +1 -1
  230. package/static/dist/assets/{dagre-GXQ25YYZ-m9agobjm.js → dagre-GXQ25YYZ-gn4nnaln.js} +4 -4
  231. package/static/dist/assets/{diagram-S7CK7UJ4-itodc50c.js → diagram-S7CK7UJ4-pa3ij6q7.js} +3 -3
  232. package/static/dist/assets/{diagram-UQ7AKVKN-n1h2t4ah.js → diagram-UQ7AKVKN-zbipyz57.js} +1 -1
  233. package/static/dist/assets/{diagram-VSXAHHWV-lvtvm9bj.js → diagram-VSXAHHWV-d1kbpzgf.js} +1 -1
  234. package/static/dist/assets/{diagram-VX7I27RA-cgas58sy.js → diagram-VX7I27RA-i3ty14xl.js} +1 -1
  235. package/static/dist/assets/{diagram-Z3DM3KII-d0aeps1q.js → diagram-Z3DM3KII-m8xefqa7.js} +1 -1
  236. package/static/dist/assets/{ebnfDiagram-PWID7BFC-md345fyq.js → ebnfDiagram-PWID7BFC-nz7wj05l.js} +1 -1
  237. package/static/dist/assets/endpoints-k7yf25m0.js +64 -0
  238. package/static/dist/assets/{erDiagram-RLTQ6QDP-i4rua63b.js → erDiagram-RLTQ6QDP-fa41ib83.js} +1 -1
  239. package/static/dist/assets/eventmodeling-NTZA5JFV-kpbq09bx.js +1 -0
  240. package/static/dist/assets/flowDiagram-HODETNUW-lcz0kx14.js +1 -0
  241. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-geqx7zgl.js → ganttDiagram-EL5Y4UJY-n7zq01qu.js} +1 -1
  242. package/static/dist/assets/{gitGraph-4MIJSDKK-leoq0z8t.js → gitGraph-4MIJSDKK-c89emb07.js} +1 -1
  243. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-llrlxfha.js → gitGraphDiagram-WWUBYQGX-cz7cgr6w.js} +1 -1
  244. package/static/dist/assets/{highlighted-body-KPVGNVTW-kq2cnw6q.js → highlighted-body-KPVGNVTW-d9bapmni.js} +1 -1
  245. package/static/dist/assets/{index-f9bzglkn.js → index-h00usru2.js} +3 -3
  246. package/static/dist/assets/index-n73ijkv2.css +2 -0
  247. package/static/dist/assets/{info-A6RAGUB7-lkly4xj6.js → info-A6RAGUB7-ndddhhhh.js} +1 -1
  248. package/static/dist/assets/infoDiagram-27XIBGKW-ccxigcpf.js +2 -0
  249. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-hq6jf374.js → ishikawaDiagram-5VMMS53U-le0z9vtc.js} +1 -1
  250. package/static/dist/assets/{journeyDiagram-3NMN7TZE-is03ntb0.js → journeyDiagram-3NMN7TZE-ivsmb26o.js} +1 -1
  251. package/static/dist/assets/{kanban-definition-UXKFOSKX-jmerizqb.js → kanban-definition-UXKFOSKX-h6n2awt0.js} +1 -1
  252. package/static/dist/assets/lazyRouteComponent-m1rtyk4t.js +1 -0
  253. package/static/dist/assets/{line-laaa8h99.js → line-dmzlfe0u.js} +1 -1
  254. package/static/dist/assets/link-budeg5t9.js +1 -0
  255. package/static/dist/assets/login-kjhasvp8.js +1 -0
  256. package/static/dist/assets/mermaid-HWGCJPDP-jb5d211j.js +1 -0
  257. package/static/dist/assets/{mermaid-parser.core-ix4junux.js → mermaid-parser.core-gwyd5bnq.js} +3 -3
  258. package/static/dist/assets/{mindmap-definition-YA3MSWOX-o0e0adl1.js → mindmap-definition-YA3MSWOX-l68gg5h7.js} +1 -1
  259. package/static/dist/assets/onboarding-cdfrk0pj.js +1 -0
  260. package/static/dist/assets/{packet-AYTQ26CC-kl8pwsyg.js → packet-AYTQ26CC-gpv06065.js} +1 -1
  261. package/static/dist/assets/{pegDiagram-XKGWAZYB-bbqn9jo4.js → pegDiagram-XKGWAZYB-e52vs23u.js} +1 -1
  262. package/static/dist/assets/{pie-WAS4IAKB-cpyo8bkr.js → pie-WAS4IAKB-l6m7nz5s.js} +1 -1
  263. package/static/dist/assets/{pieDiagram-E7YTZNPT-pelug34f.js → pieDiagram-E7YTZNPT-b4is36bq.js} +3 -3
  264. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-jds4jmr5.js → quadrantDiagram-AXDQQJYC-m8vt2788.js} +1 -1
  265. package/static/dist/assets/{radar-RG4KPBEZ-e02oxnvd.js → radar-RG4KPBEZ-nxbrhiov.js} +1 -1
  266. package/static/dist/assets/{railroad-74A4TZTK-kz3delwo.js → railroad-74A4TZTK-222mu9hi.js} +1 -1
  267. package/static/dist/assets/railroad-abnf-HS5TGJTU-kdvgl6x8.js +1 -0
  268. package/static/dist/assets/railroad-ebnf-LZEXJU2U-itiqslby.js +1 -0
  269. package/static/dist/assets/railroad-peg-WCYAUIDC-go7kov78.js +1 -0
  270. package/static/dist/assets/{railroadDiagram-O6MQD6OU-jgroj2ew.js → railroadDiagram-O6MQD6OU-o8lldfzv.js} +1 -1
  271. package/static/dist/assets/{refresh-cw-k7yxjdn3.js → refresh-cw-kbcje1uy.js} +1 -1
  272. package/static/dist/assets/{requirementDiagram-BXWQKSXE-m4az5e0j.js → requirementDiagram-BXWQKSXE-ovyq38kv.js} +1 -1
  273. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-vlryqiv3.js → sankeyDiagram-P5KCCOFB-ou7lznor.js} +1 -1
  274. package/static/dist/assets/{saved-brdfjjnw.js → saved-bazmcup9.js} +1 -1
  275. package/static/dist/assets/search-jen74z4u.js +1 -0
  276. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-fr8hrpql.js → sequenceDiagram-WJ2MYXX4-ovckdzf2.js} +1 -1
  277. package/static/dist/assets/{settings_section_extensions_title-l0zodj0b.js → settings_section_extensions_title-ertwwhlx.js} +1 -1
  278. package/static/dist/assets/share._token-dan9hgvj.js +2 -0
  279. package/static/dist/assets/{sliders-horizontal-gcgscv8r.js → sliders-horizontal-f9cb3lh3.js} +1 -1
  280. package/static/dist/assets/{stateDiagram-D77RDMKH-bg548iyr.js → stateDiagram-D77RDMKH-fcrexkkv.js} +1 -1
  281. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-lcfraua2.js +1 -0
  282. package/static/dist/assets/{swimlanes-42K2YHIH-hk5z3efh.js → swimlanes-42K2YHIH-f7usu8fw.js} +1 -1
  283. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ogmanni0.js +8 -0
  284. package/static/dist/assets/{timeline-definition-24CTP7MA-olvknr62.js → timeline-definition-24CTP7MA-gp1hvz9f.js} +1 -1
  285. package/static/dist/assets/todoStore-eqtid4r4.js +1 -0
  286. package/static/dist/assets/{treeView-Q6P3EWNA-ovvjsxtf.js → treeView-Q6P3EWNA-gb9gf11u.js} +1 -1
  287. package/static/dist/assets/{treemap-WGGIJYW6-ggkipj59.js → treemap-WGGIJYW6-nuvfnvvi.js} +1 -1
  288. package/static/dist/assets/{untitled-c8ydw0bu.js → untitled-htelhc68.js} +1 -1
  289. package/static/dist/assets/useForm-h2t0u14e.js +1 -0
  290. package/static/dist/assets/useLocale-hcsb84pj.js +1 -0
  291. package/static/dist/assets/useNavigate-nwy70ztj.js +1 -0
  292. package/static/dist/assets/{useParams-katwvzil.js → useParams-dqd64jv7.js} +1 -1
  293. package/static/dist/assets/{useSearch-c70sgau1.js → useSearch-j30pvsy7.js} +1 -1
  294. package/static/dist/assets/{vennDiagram-4TSXK5OY-hzld7zt7.js → vennDiagram-4TSXK5OY-fzchbzaj.js} +1 -1
  295. package/static/dist/assets/{wardley-WFR3VGLG-hxkeoms8.js → wardley-WFR3VGLG-m47mhfbh.js} +1 -1
  296. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-ljh90mb1.js → wardleyDiagram-VM6X3IG4-odg6ct1z.js} +2 -2
  297. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-mnuaes03.js → xychartDiagram-S5SC5T6Z-mrhms5um.js} +1 -1
  298. package/static/dist/index.html +1 -1
  299. package/static/dist/sw.js +1 -1
  300. package/static/dist/assets/AppShell-haqi9qzu.js +0 -1
  301. package/static/dist/assets/ChatPage-hjyu2g64.js +0 -22
  302. package/static/dist/assets/HubRoute-gkd8o1vg.js +0 -2
  303. package/static/dist/assets/RightPanel-o73y98dn.js +0 -1
  304. package/static/dist/assets/Select-jld3yeni.js +0 -1
  305. package/static/dist/assets/SessionListPanel-d3qx4ric.js +0 -1
  306. package/static/dist/assets/SettingsLayout-lnn2wo2s.js +0 -1
  307. package/static/dist/assets/States-e5hv6gdh.js +0 -1
  308. package/static/dist/assets/Toaster-c4jq1i26.js +0 -1
  309. package/static/dist/assets/_app-bnip2pjg.js +0 -1
  310. package/static/dist/assets/_app.ext._extensionId-egptzrfy.js +0 -1
  311. package/static/dist/assets/_app.index-vdz13ul5.js +0 -1
  312. package/static/dist/assets/_app.insights-lgjshsba.js +0 -1
  313. package/static/dist/assets/_app.kanban-c18cjrpz.js +0 -1
  314. package/static/dist/assets/_app.logs-ezhojgeg.js +0 -1
  315. package/static/dist/assets/_app.memory-i00paisv.js +0 -1
  316. package/static/dist/assets/_app.profiles-kd02bwjx.js +0 -1
  317. package/static/dist/assets/_app.settings-giaudkuk.js +0 -1
  318. package/static/dist/assets/_app.settings._section-k4qtlexk.js +0 -1
  319. package/static/dist/assets/_app.skills-bby2uvl8.js +0 -1
  320. package/static/dist/assets/_app.tasks-1kz9bosm.js +0 -2
  321. package/static/dist/assets/_app.todos-izjdbkm2.js +0 -1
  322. package/static/dist/assets/_app.workspaces-lfh2kw7s.js +0 -1
  323. package/static/dist/assets/architecture-7GRP2DOG-b4u9z1w3.js +0 -1
  324. package/static/dist/assets/architectureDiagram-5GKGNRK7-hqnf84fc.js +0 -36
  325. package/static/dist/assets/channel-o77ioktg.js +0 -1
  326. package/static/dist/assets/classDiagram-ZZMXUADV-f5rvod26.js +0 -1
  327. package/static/dist/assets/classDiagram-v2-VYDZK3BY-f5rvod26.js +0 -1
  328. package/static/dist/assets/endpoints-e58saih3.js +0 -9
  329. package/static/dist/assets/eventmodeling-NTZA5JFV-bn3fjsaq.js +0 -1
  330. package/static/dist/assets/flowDiagram-HODETNUW-li70d120.js +0 -1
  331. package/static/dist/assets/index-gkh7kum4.css +0 -2
  332. package/static/dist/assets/infoDiagram-27XIBGKW-oo7kdyte.js +0 -2
  333. package/static/dist/assets/lazyRouteComponent-b1h2ykmb.js +0 -1
  334. package/static/dist/assets/link-d8f0788e.js +0 -1
  335. package/static/dist/assets/login-llkohrcg.js +0 -1
  336. package/static/dist/assets/mermaid-HWGCJPDP-id0ffk2r.js +0 -1
  337. package/static/dist/assets/onboarding-lx0d6b38.js +0 -1
  338. package/static/dist/assets/railroad-abnf-HS5TGJTU-l13ped2z.js +0 -1
  339. package/static/dist/assets/railroad-ebnf-LZEXJU2U-c7rg3g4g.js +0 -1
  340. package/static/dist/assets/railroad-peg-WCYAUIDC-od0b8niq.js +0 -1
  341. package/static/dist/assets/search-gt2ueyvg.js +0 -1
  342. package/static/dist/assets/share._token-oyjv046r.js +0 -2
  343. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-j5v40qih.js +0 -1
  344. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-grzefo9c.js +0 -8
  345. package/static/dist/assets/tabBuild-idliyw9f.js +0 -56
  346. package/static/dist/assets/todoStore-j4c2xq0b.js +0 -1
  347. package/static/dist/assets/useForm-kd759sjk.js +0 -1
  348. package/static/dist/assets/useLocale-hds4xmja.js +0 -1
@@ -3,16 +3,23 @@ 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, 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, sessionSearchMessageText, sessionSearchPreview } from './list.js';
7
+ import { anchorSceneIntOrNull, fullToolResult, hydrateAnchorActivityScenes, normalizeAnchorSceneMessageRef, readAnchorSceneRows, storeAnchorScene, withTurnIds } from './anchor.js';
8
+ import { isSafeSessionId, lastMessageTimestamp, Session, stripAttachedFilesMarker, titleFrom } from './session.js';
9
9
  import { SessionBusy, SessionNotFound, statSignature } from './store.js';
10
10
  import { attachTodoState } from './todo.js';
11
11
  import { stateDbSessionMessages, stateDbSessionRow, stateDbSessionSources } from './state-db.js';
12
- import { attachmentObjects, mergeSessionMessagesAppendOnly, pendingUserRow, withAttachmentObjects, withPendingUserTurn, withToolCallOutcomes, withoutRunningTurnOutput } from './merge.js';
12
+ import { anchorMessageKey, anchorSummary, CompressionJobs, visibleMessagesForAnchor } from './compress.js';
13
+ 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';
15
+ import { withBackgroundUpdates } from './background-updates.js';
16
+ import { withBackgroundLinks } from './background-tasks.js';
13
17
  import { messagesForLimitedPayload, messageWindowForDisplay, MAX_MSG_LIMIT, parseMsgLimit, toolCallsForMessageWindow } from './window.js';
14
18
  import { redactText } from '../redact.js';
19
+ import { workspaceDisplayName } from '../workspace/workspaces.js';
15
20
  import { buildShareSnapshot } from './shares.js';
21
+ import { mediaAnchorRoot, mediaRefPath, mediaTarget } from '../workspace/media.js';
22
+ import { projectMediaRefs } from '../workspace/media-refs.js';
16
23
  import { loadGatewaySessionIdentityMap } from './list.js';
17
24
  import { join } from 'node:path';
18
25
  export class HttpFailure extends Error {
@@ -25,13 +32,65 @@ export class HttpFailure extends Error {
25
32
  this.name = 'HttpFailure';
26
33
  }
27
34
  }
35
+ /**
36
+ * Python `_agent_runtime_barrier_response`: a stale local Agent checkout is refused with a typed 409 before the caller
37
+ * mutates anything. Any other failure of the check is not a verdict and lets the caller proceed.
38
+ */
39
+ export async function ensureAgentRuntimeCurrent(sidecar) {
40
+ if (!sidecar)
41
+ return;
42
+ try {
43
+ await sidecar.call('runtime.ensure_current', {});
44
+ }
45
+ catch (error) {
46
+ if (error instanceof SidecarError && error.condition === 'agent_runtime_stale')
47
+ throw staleRuntimeFailure(error);
48
+ }
49
+ }
50
+ function staleRuntimeFailure(error) {
51
+ 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
+ }
28
53
  const isDict = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
29
54
  export class SessionService {
30
55
  deps;
56
+ /** TAL-255: one manual compression worker per session, shared by every compress route. */
57
+ compressionJobs;
31
58
  constructor(deps) {
32
59
  this.deps = deps;
60
+ this.compressionJobs = new CompressionJobs();
33
61
  }
34
62
  get store() { return this.deps.store; }
63
+ /**
64
+ * TAL-186: rewrites a session's media references to their URLs. A local path is rewritten only when `/api/media`
65
+ * serves it for this session, under the same resolution, so every rewritten reference loads.
66
+ */
67
+ mediaProjector(s) {
68
+ const media = this.deps.media;
69
+ const served = new Map();
70
+ const localUrl = (path) => {
71
+ if (!media?.localIo(s.profile))
72
+ return null;
73
+ if (served.has(path))
74
+ return served.get(path) ?? null;
75
+ let url = null;
76
+ try {
77
+ const target = mediaTarget(mediaRefPath(path));
78
+ if (mediaAnchorRoot(target, s, media.access))
79
+ url = `./api/media?${new URLSearchParams({ path: target, session_id: s.session_id }).toString()}`;
80
+ }
81
+ catch { /* an invalid path stays text */ }
82
+ served.set(path, url);
83
+ return url;
84
+ };
85
+ const workspace = s.workspace.trim() || null;
86
+ // A scene's final answer usually repeats its row's text: each distinct text is parsed once.
87
+ const projected = new Map();
88
+ return (text) => {
89
+ if (!projected.has(text))
90
+ projected.set(text, projectMediaRefs(text, { workspace, localUrl }));
91
+ return projected.get(text) ?? null;
92
+ };
93
+ }
35
94
  publish(reason, profile, sessionId) {
36
95
  this.deps.events.publish(reason, { profile: profile ?? null, sessionId: sessionId ?? null });
37
96
  }
@@ -135,16 +194,33 @@ export class SessionService {
135
194
  * rows (a WebUI conversation continued from the CLI shows the CLI turns). This is the coordinate space `GET
136
195
  * /api/session` exposes, so branching and the next model history slice/extend the same list.
137
196
  */
138
- mergedTranscript(s, local = s.messages) {
139
- const stateRows = this.stateDbRows(s);
197
+ mergedTranscript(s, local = s.messages, stateRows = this.stateDbRows(s)) {
140
198
  if (!stateRows.length)
141
199
  return local;
142
- return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark });
200
+ return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed });
201
+ }
202
+ /**
203
+ * Python `reconciled_state_db_messages_for_session(prefer_context=True)`: the model history is the owner context
204
+ * extended append-only with the Agent's state.db rows (a CLI continuation of this session reaches the model), except
205
+ * for a compressed context whose anchor cannot be verified — that stays context-only.
206
+ */
207
+ modelContext(s, stateRows) {
208
+ const local = s.context_messages.length ? s.context_messages : s.messages.filter((m) => !m._error && !m._partial);
209
+ return local.some((m) => isContextCompressionMarker(m)) ? local : this.mergedTranscript(s, local, stateRows ?? this.stateDbRows(s));
210
+ }
211
+ /**
212
+ * TAL-518: the model context a `/btw` clone inherits: the chat's history plus its running turn's prompt, which
213
+ * deferred save keeps out of `messages` and `context_messages` until settlement.
214
+ */
215
+ sideQuestionContext(s) {
216
+ const context = structuredClone(this.modelContext(s));
217
+ const pending = this.pendingTurn(s);
218
+ return pending ? withPendingUserTurn(context, pending) : context;
143
219
  }
144
220
  /** Python `_lookup_cli_session_metadata`: the sidebar row for a state.db session in the active profile. */
145
221
  lookupCliMeta(sid) {
146
222
  try {
147
- return this.deps.cliSessions(this.deps.activeProfile(), { sourceFilter: null }).find((r) => str(r.session_id) === sid) ?? null;
223
+ return this.deps.cliSessions(this.deps.activeProfile(), { sourceFilter: null }).rows.find((r) => str(r.session_id) === sid) ?? null;
148
224
  }
149
225
  catch {
150
226
  return null;
@@ -214,6 +290,10 @@ export class SessionService {
214
290
  messages: msgs, created_at: Number(meta.created_at) || 0, updated_at: Number(meta.updated_at) || 0, profile: str(meta.profile) || null,
215
291
  is_cli_session: claimable ? true : !subagentChild, source_tag: str(meta.source_tag) || null, raw_source: str(meta.raw_source) || null,
216
292
  session_source: str(meta.session_source) || null, source_label: str(meta.source_label) || null, read_only: !claimable,
293
+ // Python `import_cli_session`: the claimed sidecar keeps the row's lineage, background project and channel identity
294
+ // (identity persists through `Session.extra`).
295
+ parent_session_id: str(meta.parent_session_id) || null, project_id: str(meta.project_id) || null,
296
+ ...Object.fromEntries(CLI_IDENTITY_FIELDS.filter((k) => meta[k] != null && meta[k] !== '').map((k) => [k, meta[k]])),
217
297
  }, defaults);
218
298
  return { session, reason: claimable ? 'materialized' : 'not_claimable' };
219
299
  }
@@ -233,17 +313,46 @@ export class SessionService {
233
313
  if (this.isSubagentViewOnly(sid))
234
314
  throw new HttpFailure(400, `Subagent sessions are view-only and cannot be ${verb} from WebUI`);
235
315
  }
316
+ /** TAL-372: each delegation row of a turn's scene carries the status of the work it started. */
317
+ backgroundLinked(s, messages) {
318
+ return withBackgroundLinks(messages, this.deps.backgroundReceipts?.(s.session_id) ?? []);
319
+ }
236
320
  /** `compact()` plus messages, redacted for the wire (Python `_public_session_projection`). */
237
321
  publicSession(s, withMessages = true) {
238
322
  const payload = this.wireRow(s);
239
323
  // Mutation replies replace a client's transcript, so they carry the same server-built scenes as the detail.
240
324
  if (withMessages)
241
- payload.messages = hydrateAnchorActivityScenes(withToolCallOutcomes(withTurnIds(s.messages), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id });
325
+ 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) }));
242
326
  return redactSessionData(payload, this.deps.redactEnabled());
243
327
  }
244
328
  /** `compact()` with the wire streaming/read-only flags (TAL-312), for replies that return the session row. */
245
329
  wireRow(s) {
246
- return withSessionWireFlags({ ...s.compact(), read_only: this.isReadOnly(s) }, this.deps.runtime.activeStreamIds);
330
+ 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);
331
+ }
332
+ /**
333
+ * TAL-303: the label for a session's workspace, from its own profile's registry (a profileless row is the root's).
334
+ * One resolver serves one response, so each profile's registry is read once however many rows it names.
335
+ */
336
+ workspaceNames() {
337
+ const registries = new Map();
338
+ return (row) => {
339
+ const profile = str(row.profile) || 'default';
340
+ let entries = registries.get(profile);
341
+ if (!entries) {
342
+ try {
343
+ entries = this.deps.workspaces.entries(profile);
344
+ }
345
+ catch {
346
+ entries = [];
347
+ }
348
+ registries.set(profile, entries);
349
+ }
350
+ return workspaceDisplayName(str(row.workspace), entries);
351
+ };
352
+ }
353
+ /** The agent's display name for a session's profile (`assistant_name`). */
354
+ assistantName(s) {
355
+ return this.deps.assistantName(s.profile);
247
356
  }
248
357
  /** Python `public_session_projection(s.__dict__)`: every persisted field, redacted (session export). */
249
358
  publicSessionDocument(s) {
@@ -283,7 +392,7 @@ export class SessionService {
283
392
  if (journaled)
284
393
  transcript = withoutRunningTurnOutput(transcript, { ...journaled, localCount: s.messages.length });
285
394
  // Turn ids, tool outcomes and scenes are computed over the full transcript, so every window reports the same values.
286
- const all = loadMessages ? hydrateAnchorActivityScenes(withToolCallOutcomes(withTurnIds(withAttachmentObjects(transcript)), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, clipToolResults: msgLimit !== null }) : [];
395
+ 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 })) : [];
287
396
  let truncated = [];
288
397
  let offset = 0;
289
398
  let summaryCount = null;
@@ -299,6 +408,8 @@ export class SessionService {
299
408
  truncated = all.slice(prompt, offset + truncated.length);
300
409
  offset = prompt;
301
410
  }
411
+ // Per-row display fields (TAL-186 media, TAL-456 excerpts cut from that display text) only for the rows sent.
412
+ truncated = withBodyExcerpts(withDisplayMedia(truncated, this.mediaProjector(s)), s.active_stream_id);
302
413
  if (msgLimit !== null)
303
414
  truncated = messagesForLimitedPayload(truncated);
304
415
  }
@@ -317,7 +428,7 @@ export class SessionService {
317
428
  }
318
429
  const activeStreamIds = this.deps.runtime.activeStreamIds;
319
430
  const raw = {
320
- ...s.compact({ includeRuntime: true, activeStreamIds }),
431
+ ...s.compact({ includeRuntime: true, activeStreamIds, contextLengthFor: this.deps.contextLengthFor, modelOptionFor: this.deps.modelOptionFor }),
321
432
  messages: truncated,
322
433
  message_count: mergedCount,
323
434
  tool_calls: toolCalls,
@@ -344,7 +455,10 @@ export class SessionService {
344
455
  const revisionAfter = this.loadRevision(s);
345
456
  raw._load_revision = revisionBefore !== null && revisionBefore === revisionAfter ? hashRevision(revisionBefore) : `unstable-${randomUUID().replace(/-/g, '')}`;
346
457
  raw.read_only = this.isReadOnly(s);
458
+ raw.assistant_name = this.assistantName(s);
459
+ raw.workspace_name = this.workspaceNames()(s);
347
460
  withSessionWireFlags(raw, activeStreamIds);
461
+ raw.pending_steers = raw.active_stream_id ? (this.deps.runtime.pendingSteers?.(str(raw.active_stream_id)) ?? []) : [];
348
462
  return redactSessionData(raw, this.deps.redactEnabled());
349
463
  }
350
464
  /**
@@ -390,7 +504,7 @@ export class SessionService {
390
504
  foreignSessionDetail(sid) {
391
505
  const { synth, meta } = this.foreignSession(sid);
392
506
  // The same turn projection as a WebUI session: turn ids, tool outcomes, then each completed turn's scene.
393
- const msgs = hydrateAnchorActivityScenes(withToolCallOutcomes(withTurnIds(synth.messages), [], null), {});
507
+ const msgs = hydrateAnchorActivityScenes(withToolCallOutcomes(withMarkerKinds(withTurnIds(synth.messages)), [], null), {});
394
508
  const lastTs = Number(msgs[msgs.length - 1]?.timestamp ?? 0) || 0;
395
509
  const sess = {
396
510
  session_id: synth.session_id, title: synth.title, workspace: synth.workspace, model: synth.model, message_count: msgs.length,
@@ -398,6 +512,7 @@ export class SessionService {
398
512
  pinned: synth.pinned, archived: synth.archived, project_id: synth.project_id ?? null, profile: synth.profile,
399
513
  is_cli_session: synth.is_cli_session, source_tag: synth.source_tag, raw_source: synth.raw_source, session_source: synth.session_source,
400
514
  source_label: synth.source_label, read_only: synth.read_only, can_duplicate: false, messages: msgs, tool_calls: [], transcript_seq: null,
515
+ assistant_name: this.assistantName(synth), workspace_name: this.workspaceNames()(synth),
401
516
  };
402
517
  attachTodoState(sess, msgs);
403
518
  const merged = withSessionWireFlags(meta ? mergeCliSidebarMetadata(sess, meta) : sess, this.deps.runtime.activeStreamIds);
@@ -413,7 +528,7 @@ export class SessionService {
413
528
  if (this.deps.runtime.activeRunStream(session.session_id))
414
529
  return false;
415
530
  const pendingAge = session.pending_started_at ? this.deps.now() - session.pending_started_at : null;
416
- if (session.pending_user_message && pendingAge !== null && pendingAge < 30)
531
+ if (session.hasPendingPrompt && pendingAge !== null && pendingAge < 30)
417
532
  return false;
418
533
  let target = session;
419
534
  if (session.loadedMetadataOnly) {
@@ -433,7 +548,7 @@ export class SessionService {
433
548
  // Python `_materialize_pending_user_turn_before_error` (#1361): the prompt that was in flight becomes a durable user
434
549
  // turn and an interruption marker follows it, so a dead stream never silently drops what the user sent.
435
550
  const pendingText = str(target.pending_user_message);
436
- if (pendingText) {
551
+ if (target.hasPendingPrompt) {
437
552
  const turnId = str(target.active_stream_id);
438
553
  const startedAt = typeof target.pending_started_at === 'number' && target.pending_started_at > 0 ? target.pending_started_at : this.deps.now();
439
554
  const attachments = [...target.pending_attachments];
@@ -466,10 +581,12 @@ export class SessionService {
466
581
  const activeProfile = this.deps.activeProfile();
467
582
  const wantState = params.showCliSessions || params.showCronSessions || params.showWebhookSessions || params.showKanbanSessions;
468
583
  // Python reads every profile's state.db under all_profiles; this port projects the active profile only.
469
- const cliRows = wantState ? this.deps.cliSessions(activeProfile, { sourceFilter: params.sourceFilter ?? null }) : undefined;
584
+ const cliRead = wantState ? this.deps.cliSessions(activeProfile, { sourceFilter: params.sourceFilter ?? null }) : undefined;
585
+ const cliRows = cliRead?.rows;
470
586
  const gatewayIdentity = loadGatewaySessionIdentityMap(join(this.deps.profileHome(activeProfile), 'sessions', 'sessions.json'));
471
- const payload = buildSessionListPayload(this.store, { ...params, ...(cliRows ? { cliRows } : {}), gatewayIdentity, stateDbSources: this.stateDbSources, activeProfile, isolatedProfileMode: this.deps.isolatedProfileMode(), profilesMatch: this.deps.profilesMatch });
472
- return sessionListResponse(payload, this.deps.runtime, this.deps.redactEnabled(), this.deps.now());
587
+ 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 });
589
+ return sessionListResponse(payload, this.deps.runtime, this.deps.redactEnabled(), this.deps.now(), this.workspaceNames());
473
590
  }
474
591
  search(q, opts) {
475
592
  const activeProfile = this.deps.activeProfile();
@@ -478,8 +595,10 @@ export class SessionService {
478
595
  sessions = sessions.filter((r) => this.deps.profilesMatch(str(r.profile) || null, activeProfile));
479
596
  sessions = withOwnerLocks(sessions, this.stateDbSources);
480
597
  const redact = this.deps.redactEnabled();
598
+ const workspaceName = this.workspaceNames();
481
599
  const redactRow = (item) => {
482
600
  withSessionWireFlags(item, this.deps.runtime.activeStreamIds);
601
+ item.workspace_name = workspaceName(item);
483
602
  if (typeof item.title === 'string')
484
603
  item.title = redactText(item.title, redact);
485
604
  for (const f of ['display_title', '_state_db_title', 'parent_title'])
@@ -498,27 +617,61 @@ export class SessionService {
498
617
  }
499
618
  if (!opts.content)
500
619
  continue;
501
- let sess;
502
- try {
503
- sess = this.store.get(str(s.session_id), { promote: false, cacheOnMiss: false });
620
+ const hit = this.contentMatch(str(s.session_id), query, opts.depth);
621
+ if (hit)
622
+ results.push(redactRow({ ...s, ...hit }));
623
+ }
624
+ return { sessions: results, query, count: results.length, all_profiles: opts.allProfiles, active_profile: activeProfile };
625
+ }
626
+ /**
627
+ * TAL-308: the sidebar search. The candidates are exactly the `/api/sessions` rows for `params` (visibility, source,
628
+ * archived, projection and order), narrowed to `projectId` (`none`: rows without a project), then matched on the
629
+ * title, then the metadata the row shows (workspace path and name, model, provider, profile, source label), then message content.
630
+ */
631
+ sidebarSearch(q, params, opts) {
632
+ let rows = this.list(params).body.sessions;
633
+ if (opts.projectId === 'none')
634
+ rows = rows.filter((r) => !r.project_id);
635
+ else if (opts.projectId)
636
+ rows = rows.filter((r) => str(r.project_id) === opts.projectId);
637
+ const query = q.toLowerCase().trim();
638
+ const base = { all_profiles: params.allProfiles, active_profile: this.deps.activeProfile(), include_archived: params.includeArchived, sidebar_filtered: true };
639
+ if (!query)
640
+ return { ...base, sessions: rows };
641
+ const results = [];
642
+ for (const row of rows) {
643
+ if (str(row.title).toLowerCase().includes(query))
644
+ 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)))
646
+ results.push({ ...row, match_type: 'metadata' });
647
+ else if (opts.content) {
648
+ const hit = this.contentMatch(str(row.session_id), query, opts.depth);
649
+ if (hit)
650
+ results.push({ ...row, ...hit });
504
651
  }
505
- catch {
652
+ }
653
+ return { ...base, sessions: results, query, count: results.length };
654
+ }
655
+ /** The first of a stored session's first `depth` messages (0: all) containing `query`, with its redacted excerpt. */
656
+ contentMatch(sid, query, depth) {
657
+ let sess;
658
+ try {
659
+ sess = this.store.get(sid, { promote: false, cacheOnMiss: false });
660
+ }
661
+ catch {
662
+ return null;
663
+ }
664
+ for (const m of depth ? sess.messages.slice(0, depth) : sess.messages) {
665
+ const c = sessionSearchMessageText(m);
666
+ if (!c.toLowerCase().includes(query))
506
667
  continue;
507
- }
508
- const msgs = opts.depth ? sess.messages.slice(0, opts.depth) : sess.messages;
509
- for (const m of msgs) {
510
- const c = sessionSearchMessageText(m);
511
- if (c.toLowerCase().includes(query)) {
512
- const item = { ...s, match_type: 'content' };
513
- const preview = sessionSearchPreview(c, query);
514
- if (preview)
515
- item.match_preview = redactText(preview, redact);
516
- results.push(redactRow(item));
517
- break;
518
- }
519
- }
668
+ const item = { match_type: 'content' };
669
+ const preview = sessionSearchPreview(c, query);
670
+ if (preview)
671
+ item.match_preview = redactText(preview, this.deps.redactEnabled());
672
+ return item;
520
673
  }
521
- return { sessions: results, query, count: results.length, all_profiles: opts.allProfiles, active_profile: activeProfile };
674
+ return null;
522
675
  }
523
676
  // ── status / usage ───────────────────────────────────────────────────────
524
677
  status(sid) {
@@ -535,10 +688,10 @@ export class SessionService {
535
688
  catch {
536
689
  hermesHome = '';
537
690
  }
538
- const { is_streaming: streaming, active_stream_id: live, read_only: readOnly } = this.wireRow(full);
691
+ const { is_streaming: streaming, active_stream_id: live, read_only: readOnly, active_turn_origin: origin } = this.wireRow(full);
539
692
  return {
540
693
  session_id: full.session_id, title: full.title, model: full.model, profile, hermes_home: hermesHome, workspace: full.workspace, personality: full.personality,
541
- message_count: full.messages.length, created_at: full.created_at, updated_at: full.updated_at, agent_running: streaming, is_streaming: streaming, active_stream_id: live, read_only: readOnly,
694
+ message_count: full.messages.length, created_at: full.created_at, updated_at: full.updated_at, agent_running: streaming, is_streaming: streaming, active_stream_id: live, active_turn_origin: origin, read_only: readOnly,
542
695
  input_tokens: inp, output_tokens: out, total_tokens: inp + out, estimated_cost: full.estimated_cost,
543
696
  };
544
697
  }
@@ -578,7 +731,8 @@ export class SessionService {
578
731
  return toolsets;
579
732
  }
580
733
  create(body, opts = {}) {
581
- const profile = (typeof body.profile === 'string' && body.profile) || null;
734
+ // Python `new_session`: a body without a profile takes the request's active profile (cookie first).
735
+ const profile = (typeof body.profile === 'string' && body.profile) || this.deps.activeProfile();
582
736
  let prevSessionId = typeof body.prev_session_id === 'string' && body.prev_session_id ? body.prev_session_id : null;
583
737
  if (prevSessionId && !this.sessionIdVisible(prevSessionId))
584
738
  prevSessionId = null;
@@ -774,6 +928,176 @@ export class SessionService {
774
928
  this.deps.runtime.evictAgent(sid);
775
929
  return { ok: true, session: this.publicSession(s) };
776
930
  }
931
+ // ── manual compression (TAL-255) ────────────────────────────────────────
932
+ /**
933
+ * Python `_handle_session_compress_start`: validate, then join the session's running job or start a fresh one (a
934
+ * finished job is replaced). The stale-runtime refusal comes before a job exists; a running job is joined without it.
935
+ */
936
+ async startCompression(sid, focusRaw) {
937
+ const running = () => { const job = this.compressionJobs.get(sid); return job?.status === 'running' ? job : undefined; };
938
+ // A running job is joined first: once it has installed its short context, the guards below would refuse a new one.
939
+ const existing = running();
940
+ if (existing)
941
+ return existing;
942
+ const { s } = this.compressionTarget(sid);
943
+ const focusTopic = str(focusRaw).trim().slice(0, 500) || null;
944
+ await ensureAgentRuntimeCurrent(this.deps.sidecar?.() ?? null);
945
+ // Another start may have admitted a job while the runtime check awaited.
946
+ const admitted = running();
947
+ if (admitted)
948
+ return admitted;
949
+ // The detached job (and its finalize) is the profile's activity until it settles; checked and taken in one step.
950
+ const profile = s.profile ?? null;
951
+ if (this.deps.profileDeleting?.(profile))
952
+ throw new HttpFailure(409, `Profile '${str(profile)}' is being deleted.`);
953
+ const release = this.deps.profileActivity?.(profile);
954
+ const now = this.deps.now();
955
+ const job = { session_id: sid, focus_topic: focusTopic, status: 'running', started_at: now, updated_at: now, done: Promise.resolve() };
956
+ job.done = this.compressSession(sid, focusTopic).finally(() => { release?.(); this.compressionJobs.expireLater(job); }).then((result) => { Object.assign(job, { status: 'done', result, updated_at: this.deps.now() }); }, (error) => {
957
+ const known = error instanceof HttpFailure;
958
+ if (!known)
959
+ this.deps.log(`[webui] Manual compression worker failed for session ${sid}: ${error.message}`);
960
+ Object.assign(job, { status: 'error', error: known ? error.message : `Compression failed: ${sanitizePaths(error)}`, error_status: known ? error.status : 500, error_extra: known ? error.extra : {}, updated_at: this.deps.now() });
961
+ });
962
+ this.compressionJobs.set(job);
963
+ return job;
964
+ }
965
+ /** The session a compression may run on now, and the model history it compresses: the context a turn would send, so a
966
+ * repeat compression builds on the previous summary instead of re-reading the whole display transcript. */
967
+ compressionTarget(sid) {
968
+ this.rejectSubagent(sid, 'compressed');
969
+ const s = this.mutationTarget(sid, 'compressed');
970
+ if (s.active_stream_id)
971
+ throw new HttpFailure(409, 'Session is still streaming; wait for the current turn to finish.');
972
+ const history = sanitizeMessagesForApi(this.modelContext(s));
973
+ if (history.length < 4)
974
+ throw new HttpFailure(400, 'Not enough conversation to compress (need at least 4 messages).');
975
+ return { s, history };
976
+ }
977
+ /** The session's compression job; a deleted session's finished result is dropped rather than served. */
978
+ compressionJob(sid) {
979
+ try {
980
+ this.store.get(sid, { metadataOnly: true });
981
+ }
982
+ catch {
983
+ this.compressionJobs.delete(sid);
984
+ throw new HttpFailure(404, 'Session not found');
985
+ }
986
+ return this.compressionJobs.get(sid);
987
+ }
988
+ /**
989
+ * Python `_handle_session_compress`: compress the sanitized transcript in the sidecar outside the lock, then under the
990
+ * session lock refuse a result the session moved past (stream state or transcript changed) and install it as the
991
+ * model context with the manual anchor and the #4836 boundary that keeps state.db from replaying compressed rows.
992
+ * The guards run again first: a stream may have started while the caller awaited the runtime check.
993
+ */
994
+ async compressSession(sid, focusTopic) {
995
+ const { s, history } = this.compressionTarget(sid);
996
+ // The display transcript and the model context must both be where the compression found them; the key starts from
997
+ // the exact history sent, so a state.db row that lands after this read fails the commit instead of being dropped.
998
+ const transcriptKey = (x, context) => JSON.stringify([sanitizeMessagesForApi(x.messages), context]);
999
+ const historyKey = transcriptKey(s, history);
1000
+ const streamState = (x) => JSON.stringify([x.active_stream_id ?? null, x.pending_user_message ?? null, x.pending_attachments ?? null, x.pending_started_at ?? null]);
1001
+ const streamBefore = streamState(s);
1002
+ const sidecar = this.deps.sidecar?.();
1003
+ if (!sidecar)
1004
+ throw new HttpFailure(400, 'Compression failed: the Agent sidecar is not running');
1005
+ const [model, provider] = this.deps.modelStateFromRequest(s.model, s.model_provider, s.model_provider);
1006
+ let result;
1007
+ try {
1008
+ result = await sidecar.call('chat.compress', {
1009
+ profile_home: this.deps.profileHome(s.profile ?? this.deps.activeProfile()), session_id: sid, model: model ?? '', model_provider: provider,
1010
+ conversation_history: history, focus_topic: focusTopic, enabled_toolsets: s.enabled_toolsets,
1011
+ }, { timeoutMs: 0 });
1012
+ }
1013
+ catch (error) {
1014
+ if (error instanceof SidecarError && error.condition === 'agent_runtime_stale')
1015
+ throw staleRuntimeFailure(error);
1016
+ if (error instanceof SidecarError && error.condition === 'credential_missing')
1017
+ throw new HttpFailure(400, 'No provider configured -- cannot compress.');
1018
+ throw new HttpFailure(400, `Compression failed: ${sanitizePaths(error)}`);
1019
+ }
1020
+ // A held compression lock is a conflict to retry; `nothing_to_do` is the Agent's verdict on this transcript.
1021
+ if (result.status !== 'compressed')
1022
+ throw new HttpFailure(result.status === 'lock_skipped' ? 409 : 400, result.message ?? 'Nothing to compress yet.');
1023
+ const summary = result.summary ?? {};
1024
+ let committed = false;
1025
+ try {
1026
+ const current = await this.store.withLock(sid, () => {
1027
+ let live;
1028
+ try {
1029
+ live = this.store.get(sid);
1030
+ }
1031
+ catch {
1032
+ throw new HttpFailure(404, 'Session not found');
1033
+ }
1034
+ if (streamState(live) !== streamBefore)
1035
+ throw new HttpFailure(409, 'Session stream state changed during compression; please retry.');
1036
+ // One state.db read serves the check, the kept display rows, and the boundary.
1037
+ const stateRows = this.stateDbRows(live);
1038
+ if (transcriptKey(live, sanitizeMessagesForApi(this.modelContext(live, stateRows))) !== historyKey)
1039
+ throw new HttpFailure(409, 'Session was modified during compression; please retry.');
1040
+ // Rows the sidecar returns without a timestamp take the newest one this compression saw, never the clock: the
1041
+ // boundary then hides no state.db row the append-only merge would still show (it already drops rows at or
1042
+ // before the newest local row), so a CLI row committed after the read follows the compressed context.
1043
+ const seen = [...live.messages, ...live.context_messages, ...stateRows].map((m) => Number(m.timestamp)).filter(Number.isFinite);
1044
+ const stamp = seen.length ? Math.max(...seen) : this.deps.now();
1045
+ const compressed = copyJson(result.messages);
1046
+ for (const m of compressed)
1047
+ m.timestamp ??= stamp;
1048
+ live.context_messages = compressed;
1049
+ live.active_stream_id = null;
1050
+ live.pending_user_message = null;
1051
+ live.pending_attachments = [];
1052
+ live.pending_started_at = null;
1053
+ live.pending_user_source = null;
1054
+ // The compressed history included any state.db-only continuation; the new boundary would hide those rows from
1055
+ // the display, so the transcript keeps them before it is applied (and the anchor counts them).
1056
+ const display = this.mergedTranscript(live, live.messages, stateRows);
1057
+ if (display.length > live.messages.length)
1058
+ live.messages = copyJson(display);
1059
+ const visible = visibleMessagesForAnchor(live.messages);
1060
+ live.compression_anchor_visible_idx = visible.length ? visible.length - 1 : null;
1061
+ live.compression_anchor_message_key = anchorMessageKey(visible.at(-1));
1062
+ live.compression_anchor_summary = anchorSummary(summary, compressed);
1063
+ live.compression_anchor_mode = 'manual';
1064
+ // #4836: an intentional-shrink boundary, so append-only state.db reconciliation does not replay compressed rows.
1065
+ live.truncation_watermark = truncationWatermarkFor(compressed);
1066
+ live.truncation_boundary = live.truncation_watermark;
1067
+ live.truncation_watermark_compressed = true;
1068
+ live.last_prompt_tokens = result.after_tokens;
1069
+ live.post_compression_context_tokens_estimate = result.after_tokens;
1070
+ this.store.save(live);
1071
+ // A backup from before the compression would restore the uncompressed context.
1072
+ try {
1073
+ rmSync(`${this.store.pathFor(sid)}.bak`, { force: true });
1074
+ }
1075
+ catch { /* ignore */ }
1076
+ return live;
1077
+ });
1078
+ committed = true;
1079
+ // The cached turn agent still carries the uncompressed state; the next turn builds a fresh one (gateway parity).
1080
+ // Awaited, so a turn sent right after the reply cannot reuse the old agent.
1081
+ try {
1082
+ await sidecar.call('chat.evict_agent', { session_id: sid });
1083
+ }
1084
+ catch (error) {
1085
+ this.deps.log(`[webui] agent eviction after compression of ${sid} failed: ${error.message}`);
1086
+ }
1087
+ return { ok: true, session: this.publicSession(current), summary, focus_topic: focusTopic };
1088
+ }
1089
+ finally {
1090
+ // Second phase: the Agent's context-engine notification fires only for a result the session now holds.
1091
+ if (result.commit_token) {
1092
+ try {
1093
+ await sidecar.call('chat.compress_finalize', { commit_token: result.commit_token, committed });
1094
+ }
1095
+ catch (error) {
1096
+ this.deps.log(`[webui] compression finalize for ${sid} failed: ${error.message}`);
1097
+ }
1098
+ }
1099
+ }
1100
+ }
777
1101
  async clear(sid) {
778
1102
  this.rejectSubagent(sid, 'modified');
779
1103
  const s = this.mutationTarget(sid, 'modified');
@@ -810,7 +1134,7 @@ export class SessionService {
810
1134
  catch { /* ignore */ }
811
1135
  }
812
1136
  });
813
- this.deps.runtime.evictAgent(sid);
1137
+ this.deps.runtime.evictAgent(sid, true);
814
1138
  return { ok: true, session: this.wireRow(s) };
815
1139
  }
816
1140
  async retry(sid) {
@@ -821,11 +1145,19 @@ export class SessionService {
821
1145
  const lastUser = findLastUserIndex(history);
822
1146
  if (lastUser === null)
823
1147
  return { error: 'No previous message to retry.' };
824
- const lastUserText = extractText(history[lastUser]?.content);
1148
+ // What a client resends (TAL-515): the files that reached the model, which only ever got attachments with a path,
1149
+ // and the prompt as typed. The attached-files line is the server's only when such files exist; otherwise the user
1150
+ // typed it. Nothing to resend fails before the exchange is removed.
1151
+ const prompt = history[lastUser];
1152
+ const lastUserText = extractText(prompt.content);
1153
+ const lastUserAttachments = attachmentObjects(Array.isArray(prompt.attachments) ? prompt.attachments : []).filter((a) => isDict(a) && Boolean(str(a.path).trim()));
1154
+ const lastUserPrompt = stripWorkspacePrefix(lastUserAttachments.length ? stripAttachedFilesMarker(lastUserText) : lastUserText, true);
1155
+ if (!lastUserPrompt && !lastUserAttachments.length)
1156
+ return { error: 'The last message has nothing to resend.' };
825
1157
  const removed = history.length - lastUser;
826
1158
  shrinkTo(s, lastUser);
827
1159
  this.store.save(s);
828
- return { ok: true, last_user_text: lastUserText, removed_count: removed };
1160
+ return { ok: true, last_user_text: lastUserText, last_user_prompt: lastUserPrompt, last_user_attachments: lastUserAttachments, removed_count: removed };
829
1161
  });
830
1162
  }
831
1163
  async undo(sid) {
@@ -850,12 +1182,14 @@ export class SessionService {
850
1182
  if (!session)
851
1183
  throw new HttpFailure(404, 'Session not found');
852
1184
  const now = this.deps.now();
1185
+ // A truncation cut names deleted state.db rows of the source; the copy's own state.db rows are all genuine (TAL-504).
1186
+ const watermark = session.truncation_watermark_compressed ? { truncation_watermark: session.truncation_watermark, truncation_boundary: session.truncation_boundary, truncation_watermark_compressed: true } : {};
853
1187
  const copied = new Session({
854
1188
  title: `${session.title || 'Untitled'} (copy)`, workspace: session.workspace, model: session.model, model_provider: session.model_provider,
855
1189
  messages: copyJson(session.messages), tool_calls: copyJson(session.tool_calls), pinned: false, archived: false, project_id: session.project_id, profile: session.profile,
856
1190
  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,
857
1191
  personality: session.personality, enabled_toolsets: session.enabled_toolsets, context_length: session.context_length, threshold_tokens: session.threshold_tokens,
858
- truncation_watermark: session.truncation_watermark, truncation_boundary: session.truncation_boundary, context_messages: copyJson(session.context_messages),
1192
+ ...watermark, context_messages: copyJson(session.context_messages),
859
1193
  gateway_routing: copyJson(session.gateway_routing), gateway_routing_history: copyJson(session.gateway_routing_history), llm_title_generated: session.llm_title_generated,
860
1194
  manual_title: session.manual_title, composer_draft: copyJson(session.composer_draft), context_engine: session.context_engine, context_engine_state: copyJson(session.context_engine_state),
861
1195
  created_at: now, updated_at: now,
@@ -930,11 +1264,13 @@ export class SessionService {
930
1264
  return {};
931
1265
  } })();
932
1266
  let eventProfile = null;
1267
+ let hadSidecar = true;
933
1268
  try {
934
1269
  eventProfile = this.store.get(sid, { metadataOnly: true }).profile;
935
1270
  }
936
1271
  catch {
937
1272
  eventProfile = null;
1273
+ hadSidecar = false;
938
1274
  }
939
1275
  // Python `_is_messaging_session_id`: decided before the JSON is gone, from WebUI metadata or the Agent's row.
940
1276
  const isMessaging = (() => { try {
@@ -961,6 +1297,14 @@ export class SessionService {
961
1297
  await this.store.withLock(sid, () => {
962
1298
  if (blocking())
963
1299
  throw new HttpFailure(409, 'Session has an active run; stop it before deleting');
1300
+ // A public share outlives its session file, and revokeShare needs the session, so revoke it first.
1301
+ try {
1302
+ this.deps.shares.revoke(this.store.get(sid, { metadataOnly: true }));
1303
+ }
1304
+ catch (error) {
1305
+ if (!(error instanceof SessionNotFound))
1306
+ throw error;
1307
+ }
964
1308
  if (!this.store.deleteFiles(sid))
965
1309
  throw new HttpFailure(500, 'Failed to delete session data');
966
1310
  }, { timeoutMs: 5000 });
@@ -970,7 +1314,7 @@ export class SessionService {
970
1314
  throw new HttpFailure(503, 'Session busy, try again');
971
1315
  throw error;
972
1316
  }
973
- this.deps.runtime.evictAgent(sid);
1317
+ this.deps.runtime.evictAgent(sid, true);
974
1318
  try {
975
1319
  rmSync(this.deps.attachmentDir(sid), { recursive: true, force: true });
976
1320
  }
@@ -993,6 +1337,9 @@ export class SessionService {
993
1337
  }
994
1338
  }
995
1339
  this.publish('session_delete', eventProfile);
1340
+ // Only a sidecar session is ever published to the relay.
1341
+ if (hadSidecar)
1342
+ this.deps.clearRelayCompletions?.(sid, eventProfile);
996
1343
  return { ok: true, state_db_cleanup_failed: stateDbCleanupFailed, ...retained };
997
1344
  }
998
1345
  cleanup(zeroOnly) {
@@ -1002,6 +1349,13 @@ export class SessionService {
1002
1349
  const shouldDelete = zeroOnly ? s.messages.length === 0 : s.title === 'Untitled' && s.messages.length === 0;
1003
1350
  if (!shouldDelete)
1004
1351
  continue;
1352
+ // A cleared session keeps its share; keep the session (and its revoke route) if the share cannot be revoked.
1353
+ try {
1354
+ this.deps.shares.revoke(s);
1355
+ }
1356
+ catch {
1357
+ continue;
1358
+ }
1005
1359
  this.store.sessions.delete(s.session_id);
1006
1360
  rmSync(this.store.pathFor(s.session_id), { force: true });
1007
1361
  cleaned += 1;
@@ -1202,36 +1556,50 @@ export class SessionService {
1202
1556
  return { ok: true, ...placement };
1203
1557
  });
1204
1558
  }
1205
- readAnchorScene(query) {
1206
- const sid = str(query.session_id).trim();
1207
- const messageRef = normalizeAnchorSceneMessageRef(query.message_ref);
1208
- const messageIndex = anchorSceneIntOrNull(query.message_index);
1209
- if (!sid || (!messageRef && messageIndex === null))
1210
- throw new HttpFailure(400, 'session_id and message_ref or message_index are required');
1211
- let session;
1212
- let transcript;
1559
+ /** The session and the full transcript its detail is built from; a state.db-only session gives its synthesized one. */
1560
+ fullTranscript(sid) {
1213
1561
  try {
1214
- session = this.store.get(sid);
1562
+ let session = this.store.get(sid);
1215
1563
  if (session.loadedMetadataOnly)
1216
1564
  session = this.store.load(sid) ?? session;
1217
1565
  if (!this.visibleToActiveProfile(session.profile))
1218
1566
  throw new HttpFailure(404, 'Session not found');
1219
- transcript = this.mergedTranscript(session);
1567
+ return { session, transcript: this.mergedTranscript(session), stored: true };
1220
1568
  }
1221
1569
  catch (error) {
1222
1570
  if (error instanceof HttpFailure)
1223
1571
  throw error;
1224
- // A state.db-only session pages the same synthesized transcript its detail was built from.
1225
- session = this.foreignSession(sid).synth;
1226
- transcript = session.messages;
1572
+ const session = this.foreignSession(sid).synth;
1573
+ return { session, transcript: session.messages, stored: false };
1227
1574
  }
1575
+ }
1576
+ readAnchorScene(query) {
1577
+ const sid = str(query.session_id).trim();
1578
+ const messageRef = normalizeAnchorSceneMessageRef(query.message_ref);
1579
+ const messageIndex = anchorSceneIntOrNull(query.message_index);
1580
+ if (!sid || (!messageRef && messageIndex === null))
1581
+ throw new HttpFailure(400, 'session_id and message_ref or message_index are required');
1582
+ const { session, transcript, stored } = this.fullTranscript(sid);
1228
1583
  const result = readAnchorSceneRows(session, { messageRef, messageIndex, before: anchorSceneIntOrNull(query.before), limit: anchorSceneIntOrNull(query.limit) }, withToolCallOutcomes(withTurnIds(transcript), session.tool_calls, session.active_stream_id));
1229
1584
  if (!result)
1230
1585
  throw new HttpFailure(404, 'Anchor activity scene not found');
1231
1586
  // Paged rows come from the raw transcript, so they take the same credential redaction as the detail's preview.
1232
1587
  const enabled = this.deps.redactEnabled();
1233
1588
  const redacted = redactValue(result, enabled);
1234
- return { ...redacted, rows: withSceneToolDisplay(result.rows, redacted.rows, enabled) };
1589
+ const rows = withSceneToolDisplay(result.rows, redacted.rows, enabled);
1590
+ // `/api/media` serves stored sessions only, so a state.db-only session's rows stay as written, as its detail does.
1591
+ return { ...redacted, rows: stored ? withSceneRowMedia(rows, this.mediaProjector(session)) : rows };
1592
+ }
1593
+ /** TAL-331: one tool call's whole result, which a limited response clipped (`result_truncated`), redacted like the detail. */
1594
+ readToolResult(query) {
1595
+ const sid = query.session_id.trim();
1596
+ const id = query.tool_call_id.trim();
1597
+ if (!sid || !id)
1598
+ throw new HttpFailure(400, 'session_id and tool_call_id are required');
1599
+ const full = fullToolResult(this.fullTranscript(sid).transcript, id);
1600
+ if (full === null)
1601
+ throw new HttpFailure(404, 'Tool result not found');
1602
+ return { tool_call_id: id, ...redactValue(full, this.deps.redactEnabled()) };
1235
1603
  }
1236
1604
  // ── shares ───────────────────────────────────────────────────────────────
1237
1605
  createShare(sid) {
@@ -1316,6 +1684,10 @@ function findLastUserIndex(history) {
1316
1684
  return i;
1317
1685
  return null;
1318
1686
  }
1687
+ /** Python `_sanitize_error`: absolute paths in an error message never reach the client. */
1688
+ export function sanitizePaths(error) {
1689
+ return str(error?.message ?? error).replace(/(?:(?:\/[a-zA-Z0-9_.-]+)+|(?:[A-Z]:\\[^\s]+))/g, '<path>');
1690
+ }
1319
1691
  function truncationWatermarkFor(messages) {
1320
1692
  if (!messages.length)
1321
1693
  return 0.0;
@@ -1329,17 +1701,142 @@ function stampIntentionalShrink(session, oldCount, newCount) {
1329
1701
  session.intentional_shrink_generation = randomUUID().replace(/-/g, '');
1330
1702
  return true;
1331
1703
  }
1332
- /** Align model context with the display prefix (Python `truncate_context_for_display_keep`, simplified to the row-count contract). */
1704
+ /** Python truthiness for the JSON values a transcript row carries. */
1705
+ function pyTruthy(v) {
1706
+ if (Array.isArray(v))
1707
+ return v.length > 0;
1708
+ if (isDict(v))
1709
+ return Object.keys(v).length > 0;
1710
+ return Boolean(v);
1711
+ }
1712
+ /** JSON with sorted object keys (Python `json.dumps(..., sort_keys=True)`). */
1713
+ function sortedJson(v) {
1714
+ return JSON.stringify(v, (_key, value) => (isDict(value) ? Object.fromEntries(Object.keys(value).sort().map((k) => [k, value[k]])) : value)) ?? '';
1715
+ }
1716
+ /** Equality key for a row `id`/`timestamp`; `null` when the value is absent. Numbers compare by value (`2 == 2.0`). */
1717
+ function identityKey(v) {
1718
+ if (v === null || v === undefined)
1719
+ return null;
1720
+ return typeof v === 'string' ? `s${v}` : typeof v === 'number' ? `n${String(v)}` : `j${sortedJson(v)}`;
1721
+ }
1722
+ function rowSignature(row) {
1723
+ if (!isDict(row))
1724
+ return null;
1725
+ const field = (v) => (!pyTruthy(v) ? '' : typeof v === 'string' ? v : sortedJson(v));
1726
+ 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)]);
1727
+ }
1728
+ /** First index in sorted `positions` whose value is `>= start`. */
1729
+ function lowerBound(positions, start) {
1730
+ let lo = 0;
1731
+ let hi = positions.length;
1732
+ while (lo < hi) {
1733
+ const mid = (lo + hi) >> 1;
1734
+ if (positions[mid] < start)
1735
+ lo = mid + 1;
1736
+ else
1737
+ hi = mid;
1738
+ }
1739
+ return lo;
1740
+ }
1741
+ /**
1742
+ * Map each display row to the context row it represents, scanning forward. An `id` match wins; otherwise the role+content
1743
+ * signature must match, and a shared `timestamp` makes it exact. A signature match where an identity is missing is weak:
1744
+ * one weak candidate counts as a match, a second one before any exact match leaves the row ambiguous.
1745
+ */
1746
+ function alignDisplayToContext(ctx, full) {
1747
+ const index = () => new Map();
1748
+ const push = (map, key, idx) => {
1749
+ const list = map.get(key);
1750
+ if (list)
1751
+ list.push(idx);
1752
+ else
1753
+ map.set(key, [idx]);
1754
+ };
1755
+ const first = (list, start) => list?.[lowerBound(list, start)];
1756
+ const byId = index(), bySigTs = index(), bySigTsNoId = index();
1757
+ const weakAny = index(), weakNoId = index(), weakNoTs = index(), weakNoIdNoTs = index();
1758
+ ctx.forEach((row, idx) => {
1759
+ const sig = rowSignature(row);
1760
+ if (sig === null)
1761
+ return;
1762
+ const id = identityKey(row.id);
1763
+ const ts = identityKey(row.timestamp);
1764
+ push(weakAny, sig, idx);
1765
+ if (id === null)
1766
+ push(weakNoId, sig, idx);
1767
+ if (ts === null)
1768
+ push(weakNoTs, sig, idx);
1769
+ if (id === null && ts === null)
1770
+ push(weakNoIdNoTs, sig, idx);
1771
+ if (id !== null)
1772
+ push(byId, id, idx);
1773
+ if (ts !== null) {
1774
+ push(bySigTs, `${sig}\0${ts}`, idx);
1775
+ if (id === null)
1776
+ push(bySigTsNoId, `${sig}\0${ts}`, idx);
1777
+ }
1778
+ });
1779
+ const matches = [];
1780
+ const ambiguous = [];
1781
+ let next = 0;
1782
+ for (const message of full) {
1783
+ let match = null;
1784
+ let ambiguousAt = null;
1785
+ const sig = rowSignature(message);
1786
+ if (sig !== null) {
1787
+ const id = identityKey(message.id);
1788
+ const ts = identityKey(message.timestamp);
1789
+ const exact = [id !== null ? first(byId.get(id), next) : undefined, ts !== null ? first((id !== null ? bySigTsNoId : bySigTs).get(`${sig}\0${ts}`), next) : undefined]
1790
+ .filter((idx) => idx !== undefined);
1791
+ const exactIdx = exact.length ? Math.min(...exact) : null;
1792
+ const weakList = (id !== null ? (ts !== null ? weakNoIdNoTs : weakNoId) : ts !== null ? weakNoTs : weakAny).get(sig) ?? [];
1793
+ const w = lowerBound(weakList, next);
1794
+ const [firstWeak, secondWeak] = [weakList[w], weakList[w + 1]];
1795
+ if (secondWeak !== undefined && (exactIdx === null || secondWeak < exactIdx))
1796
+ ambiguousAt = firstWeak;
1797
+ else
1798
+ match = exactIdx ?? firstWeak ?? null;
1799
+ }
1800
+ matches.push(match);
1801
+ ambiguous.push(ambiguousAt);
1802
+ if (match !== null)
1803
+ next = match + 1;
1804
+ }
1805
+ return { matches, ambiguous };
1806
+ }
1807
+ /** Align model context with the display prefix `full[:keep]` (Python `truncate_context_for_display_keep`, #5096/#5563). */
1333
1808
  export function truncateContextForDisplayKeep(context, full, keep) {
1334
1809
  if (keep <= 0)
1335
1810
  return [];
1336
- const rows = context ?? [];
1337
- if (!rows.length)
1811
+ const ctx = context ?? [];
1812
+ if (!ctx.length || !full.length)
1338
1813
  return [];
1339
- if (keep >= full.length)
1340
- return [...rows];
1341
- // ponytail: context rows map 1:1 onto display rows for WebUI-authored transcripts; fuzzy alignment stays in Python until compression lands.
1342
- return rows.slice(0, Math.min(rows.length, keep));
1814
+ if (keep < full.length) {
1815
+ const { matches, ambiguous } = alignDisplayToContext(ctx, full);
1816
+ const lastKept = matches[keep - 1];
1817
+ const firstUnkept = matches[keep];
1818
+ const boundary = full[keep - 1];
1819
+ const keptUser = isDict(boundary) && boundary.role === 'user';
1820
+ // Cut at the first unkept display row; a kept user turn ends at its own row so unkept tool rows after it drop.
1821
+ if (firstUnkept !== null)
1822
+ return lastKept !== null && keptUser ? ctx.slice(0, lastKept + 1) : ctx.slice(0, firstUnkept);
1823
+ if (lastKept !== null) {
1824
+ const ambiguousUnkept = ambiguous[keep];
1825
+ return ambiguousUnkept !== null && isDict(boundary) && !keptUser ? ctx.slice(0, ambiguousUnkept) : ctx.slice(0, lastKept + 1);
1826
+ }
1827
+ // Both boundary rows unresolved in a shorter (compressed) context: cut past the last kept display row that resolved,
1828
+ // accepting a weak match. Under-keeping beats slicing at the raw display index.
1829
+ if (ctx.length < full.length) {
1830
+ for (let i = keep - 1; i >= 0; i -= 1) {
1831
+ const resolved = matches[i] ?? ambiguous[i];
1832
+ if (resolved !== null && resolved !== undefined)
1833
+ return ctx.slice(0, resolved + 1);
1834
+ }
1835
+ }
1836
+ }
1837
+ // Unalignable (or nothing cut): keep the compaction prefix a longer context carries, then `keep` rows (#5096).
1838
+ const prefixLen = Math.max(0, ctx.length - full.length);
1839
+ return ctx.slice(0, prefixLen + keep);
1343
1840
  }
1344
1841
  export function truncateSessionAtKeep(session, keep) {
1345
1842
  const full = [...session.messages];
@@ -1350,6 +1847,7 @@ export function truncateSessionAtKeep(session, keep) {
1350
1847
  session.context_messages = truncateContextForDisplayKeep(session.context_messages, full, keep);
1351
1848
  session.truncation_watermark = truncationWatermarkFor(session.messages);
1352
1849
  session.truncation_boundary = session.truncation_watermark;
1850
+ session.truncation_watermark_compressed = false;
1353
1851
  return [oldMsgCount, oldCtxCount];
1354
1852
  }
1355
1853
  function shrinkTo(session, lastUserIdx) {
@@ -1358,6 +1856,7 @@ function shrinkTo(session, lastUserIdx) {
1358
1856
  stampIntentionalShrink(session, history.length, session.messages.length);
1359
1857
  session.truncation_watermark = truncationWatermarkFor(session.messages);
1360
1858
  session.truncation_boundary = session.truncation_watermark;
1859
+ session.truncation_watermark_compressed = false;
1361
1860
  if (session.context_messages.length) {
1362
1861
  const ctxLast = findLastUserIndex(session.context_messages);
1363
1862
  if (ctxLast !== null)