@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
@@ -11,14 +11,18 @@ through ``approval.respond`` / ``clarify.respond``; ``rpc.cancel`` or
11
11
  from __future__ import annotations
12
12
 
13
13
  import contextlib
14
+ import contextvars
15
+ import copy
14
16
  import hashlib
15
17
  import inspect
16
18
  import json
17
19
  import logging
20
+ import os
18
21
  import threading
19
22
  import time
20
23
  import uuid
21
24
  from collections import OrderedDict
25
+ from pathlib import Path
22
26
  from typing import Any
23
27
 
24
28
  from ..errors import InvalidParams, RpcError
@@ -28,6 +32,10 @@ from ..rpc import CallContext
28
32
  log = logging.getLogger("talaria_sidecar.chat")
29
33
 
30
34
  _AGENT_CACHE_MAX = 32
35
+ # Shutdown waits this long for evicted agents' memory providers; the server kills the sidecar 5 s after closing stdin.
36
+ _AGENT_RELEASE_TIMEOUT = 4.0
37
+ # Profile deletion waits this long for the profile's memory flushes, under the server's 120 s call timeout.
38
+ _PROFILE_RELEASE_TIMEOUT = 60.0
31
39
  _TOOL_RESULT_SNIPPET_MAX = 4000
32
40
  # Predecessor ``_TOOL_ARG_CONTENT_KEYS`` (#4928): card content / diff-reconstruction inputs keep the long cap.
33
41
  _TOOL_ARG_CONTENT_KEYS = frozenset({"command", "cmd", "script", "code", "patch", "diff", "old_string", "new_string", "content", "path", "file_path"})
@@ -64,8 +72,16 @@ _RUNS_BY_SESSION: dict[str, str] = {}
64
72
  _APPROVAL_CB_OWNER: dict[str, str] = {}
65
73
  _APPROVAL_CB_LOCK = threading.Lock()
66
74
  _RUNS_LOCK = threading.Lock()
67
- _AGENT_CACHE: "OrderedDict[str, tuple[Any, str]]" = OrderedDict()
75
+ # session id -> (agent, signature, profile home).
76
+ _AGENT_CACHE: "OrderedDict[str, tuple[Any, str, Any]]" = OrderedDict()
68
77
  _AGENT_CACHE_LOCK = threading.Lock()
78
+ # Running release thread -> the profile home it runs under.
79
+ _RELEASES: dict[threading.Thread, Any] = {}
80
+ _RELEASES_LOCK = threading.Lock()
81
+ # Agents inside ``chat.commit_memory`` (guarded by ``_AGENT_CACHE_LOCK``): like a turn, a commit defers their release.
82
+ _COMMITTING: list = []
83
+ # Set (under ``_RUNS_LOCK``) when shutdown drains: no turn is admitted after the drain's snapshot of ``_RUNS``.
84
+ _DRAINING = threading.Event()
69
85
 
70
86
 
71
87
  def _snippet(raw: Any, limit: int = _TOOL_RESULT_SNIPPET_MAX) -> str:
@@ -87,6 +103,16 @@ _RAW_RESULT_MAX_KEYS = 64
87
103
  _RAW_RESULT_OUTCOME_KEYS = ("error", "exit_code", "exitCode", "success")
88
104
 
89
105
 
106
+ def _result_diff(raw: Any) -> str | None:
107
+ """A dict (or JSON-object text) result's string ``diff`` field, whole; the server decides what it shows."""
108
+ try:
109
+ data = raw if isinstance(raw, dict) else json.loads(str(raw or ""))
110
+ except Exception: # noqa: BLE001
111
+ return None
112
+ diff = data.get("diff") if isinstance(data, dict) else None
113
+ return diff if isinstance(diff, str) else None
114
+
115
+
90
116
  def _raw_result(raw: Any, limit: int = _TOOL_RESULT_SNIPPET_MAX) -> Any:
91
117
  """The tool result as the server's outcome rule reads it, bounded: a dict (or JSON-object text) keeps its first
92
118
  ``_RAW_RESULT_MAX_KEYS`` top-level fields plus its outcome fields, scalars as they are, text and non-empty nested
@@ -231,6 +257,98 @@ def _context_length(agent: Any) -> dict:
231
257
  return out
232
258
 
233
259
 
260
+ # TAL-539: the hard cap on raw tool output kept in a compressed context (predecessor ``_hard_prune_post_compression_*``).
261
+ _TAIL_TOOL_BUDGET_TOKENS = 4096
262
+ _TAIL_TOOL_MIN_SNIPPET_TOKENS = 256
263
+ _ROUGH_TOKEN_CHARS = 4
264
+
265
+
266
+ def _content_text(content: Any) -> str:
267
+ if isinstance(content, list):
268
+ return " ".join(str(p.get("text") or "") for p in content if isinstance(p, dict))
269
+ return str(content or "")
270
+
271
+
272
+ def _rough_tokens(text: str) -> int:
273
+ """The Agent's estimate, which counts CJK and other non-ASCII text as denser; ceil(chars / 4) without it."""
274
+ try:
275
+ from agent.model_metadata import estimate_tokens_rough
276
+
277
+ return int(estimate_tokens_rough(text))
278
+ except Exception: # noqa: BLE001
279
+ return -(-len(text) // _ROUGH_TOKEN_CHARS)
280
+
281
+
282
+ def _capped_tool_result(text: str, tokens: int, keep_tokens: int) -> str:
283
+ """The kept head of an over-budget tool result plus a note on what the model no longer sees."""
284
+ prefix = "[Talaria context budget: omitted "
285
+ suffix = f" (~{tokens} rough tokens) from this tool result; the full output remains in the visible transcript.]"
286
+ if keep_tokens < _TAIL_TOOL_MIN_SNIPPET_TOKENS:
287
+ return f"{prefix}{len(text)} chars{suffix}"
288
+ room = keep_tokens - _rough_tokens(f"{prefix}{len(text)} of {len(text)} chars{suffix}") - 1
289
+ snippet = text[: len(text) * room // tokens]
290
+ # Density varies along the text: shrink until the kept head fits the remaining tokens.
291
+ while snippet and _rough_tokens(snippet) > room:
292
+ snippet = snippet[: len(snippet) * 3 // 4]
293
+ snippet = snippet.rstrip()
294
+ if not snippet:
295
+ return f"{prefix}{len(text)} chars{suffix}"
296
+ return f"{snippet}\n\n{prefix}{len(text) - len(snippet)} of {len(text)} chars{suffix}"
297
+
298
+
299
+ def _cap_tool_results(messages: list, compressor: Any) -> list:
300
+ """Newest tool results spend one shared budget; the first one over it is cut to the remainder, older ones to a note."""
301
+ budget = getattr(compressor, "tail_token_budget", None)
302
+ budget = budget if isinstance(budget, int) and budget > 0 else _TAIL_TOOL_BUDGET_TOKENS
303
+ threshold = getattr(compressor, "threshold_tokens", None)
304
+ if isinstance(threshold, int) and threshold > 0:
305
+ budget = min(budget, max(512, threshold // 2))
306
+ budget = max(512, budget)
307
+ spent = 0
308
+ out = list(messages)
309
+ for i in range(len(out) - 1, -1, -1):
310
+ msg = out[i]
311
+ if msg.get("role") != "tool":
312
+ continue
313
+ text = _content_text(msg.get("content"))
314
+ if not text.strip():
315
+ continue
316
+ tokens = _rough_tokens(text)
317
+ if tokens <= budget - spent:
318
+ spent += tokens
319
+ continue
320
+ out[i] = {**msg, "content": _capped_tool_result(text, tokens, budget - spent)}
321
+ spent = budget
322
+ return out
323
+
324
+
325
+ def _post_compression_context(agent: Any, messages: list, system_message: Any) -> tuple[list, int | None]:
326
+ """TAL-539: tool output produced after a mid-turn compression never went through the compressor's pruning. Run that
327
+ pruning plus a hard cap on the model context, and estimate the next request from it (predecessor
328
+ ``_prune_context_tool_results_after_compression`` / ``_estimate_post_compression_context_tokens``)."""
329
+ compressor = getattr(agent, "context_compressor", None)
330
+ context = messages
331
+ prune = getattr(compressor, "_prune_old_tool_results", None)
332
+ if callable(prune):
333
+ try:
334
+ pruned, count = prune(copy.deepcopy(messages), protect_tail_count=getattr(compressor, "protect_last_n", 20), protect_tail_tokens=getattr(compressor, "tail_token_budget", None))
335
+ if count and isinstance(pruned, list) and len(pruned) == len(messages):
336
+ context = pruned
337
+ except Exception: # noqa: BLE001 - the hard cap below still bounds the context
338
+ log.debug("post-compression tool-result pruning failed", exc_info=True)
339
+ context = _cap_tool_results(context, compressor)
340
+ estimate = None
341
+ try:
342
+ from agent.model_metadata import estimate_request_tokens_rough
343
+
344
+ # The request carries the ephemeral prompt (surface, personality, delivery) after the system message.
345
+ prompts = [p for p in (system_message, getattr(agent, "ephemeral_system_prompt", None)) if isinstance(p, str) and p.strip()]
346
+ estimate = estimate_request_tokens_rough(context, system_prompt="\n\n".join(prompts), tools=getattr(agent, "tools", None) or None)
347
+ except Exception: # noqa: BLE001 - display-only figure
348
+ log.debug("post-compression context estimate failed", exc_info=True)
349
+ return context, estimate if isinstance(estimate, int) and estimate > 0 else None
350
+
351
+
234
352
  def _clarify_timeout(params: dict) -> int:
235
353
  """Predecessor ``_clarify_timeout_seconds``: an explicit request value wins, else the Agent's resolver over config."""
236
354
  explicit = params.get("clarify_timeout_seconds")
@@ -372,7 +490,7 @@ def _resolve_runtime(provider: str | None, model: str) -> dict:
372
490
  def _turn_identity(session_id: str, workspace: str):
373
491
  """Bind this turn's identity to the calling context the way the predecessor's ``_set_turn_session_identity`` did:
374
492
  the approval session key (so ``register_gateway_notify`` cards reach this turn and dangerous commands are gated
375
- instead of auto-approved), the gateway session vars (platform ``webui``, chat/ui session ids), and the session cwd
493
+ instead of auto-approved), the gateway session vars (platform ``webui``, chat/ui session ids, profile), and the session cwd
376
494
  (so terminals and AGENTS.md discovery run in the selected workspace, not the sidecar's launch directory). Every
377
495
  binding is a context variable, so concurrent turns cannot overwrite each other. Missing Agent surfaces are logged,
378
496
  not fatal."""
@@ -388,10 +506,14 @@ def _turn_identity(session_id: str, workspace: str):
388
506
  log.debug("per-turn approval session-key bind failed", exc_info=True)
389
507
  try:
390
508
  from gateway import session_context as sc
509
+ from hermes_cli.profiles import get_active_profile_name
510
+ from hermes_constants import get_hermes_home, profile_name_for_home
391
511
 
512
+ # The persistent Docker sandbox is keyed by the profile name; unbound, every profile reuses "default".
513
+ profile = profile_name_for_home(get_hermes_home()) or get_active_profile_name()
392
514
  pairs = [
393
515
  (sc._SESSION_KEY, session_id), (sc._SESSION_UI_SESSION_ID, session_id), (sc._SESSION_PLATFORM, "webui"),
394
- (sc._SESSION_CHAT_ID, session_id), (sc._SESSION_ID, session_id),
516
+ (sc._SESSION_CHAT_ID, session_id), (sc._SESSION_ID, session_id), (sc._SESSION_PROFILE, profile),
395
517
  ]
396
518
  for var, value in pairs:
397
519
  tok = var.set(value)
@@ -415,6 +537,87 @@ def _turn_identity(session_id: str, workspace: str):
415
537
  log.debug("per-turn identity reset failed", exc_info=True)
416
538
 
417
539
 
540
+ #: Older configs named the CLI composite ``hermes``; the Agent registers it split in two (predecessor
541
+ #: ``_LEGACY_CLI_TOOLSET_ALIASES``).
542
+ _LEGACY_TOOLSET_ALIASES = {"hermes": ("hermes-cli", "hermes-api-server")}
543
+
544
+
545
+ def _turn_toolsets(toolsets) -> list[str]:
546
+ """The session's override as sent, else the profile's configured toolsets. ``None`` would hand the Agent every
547
+ registered toolset."""
548
+ return toolsets if isinstance(toolsets, list) else _profile_toolsets()
549
+
550
+
551
+ def _profile_toolsets() -> list[str]:
552
+ """The profile's ``platform_toolsets.cli`` through the Agent's own resolver (default-off toolsets stay off), like
553
+ ``hermes chat``. Runs under the call's ``scoped_home``."""
554
+ from hermes_cli.config import load_config
555
+ from hermes_cli.tools_config import _get_platform_tools
556
+
557
+ resolved = sorted(_get_platform_tools(load_config() or {}, "cli"))
558
+ return list(dict.fromkeys(name for raw in resolved for name in _LEGACY_TOOLSET_ALIASES.get(raw, (raw,))))
559
+
560
+
561
+ def _discover_mcp_tools(run: _Run) -> None:
562
+ """Connect the profile's MCP servers before the turn's agent snapshots its tools, like the predecessor's turn start.
563
+ The Agent keys connections by profile scope and skips live ones, so later turns are cheap. Runs under the call's
564
+ ``scoped_home``; a failure leaves the turn without MCP tools rather than failing it. A Stop does not wait for a slow
565
+ server: the turn moves on to its cancelled end while the connect finishes in the background."""
566
+ from .commands import retire_unscoped_launch_mcp_servers
567
+
568
+ done = threading.Event()
569
+
570
+ def discover() -> None:
571
+ try:
572
+ from tools.mcp_tool_discovery import discover_mcp_tools
573
+
574
+ retire_unscoped_launch_mcp_servers()
575
+ discover_mcp_tools()
576
+ except Exception: # noqa: BLE001
577
+ log.warning("MCP discovery failed", exc_info=True)
578
+ finally:
579
+ done.set()
580
+
581
+ # The copied context carries the turn's home override and secret scope into the worker.
582
+ threading.Thread(target=contextvars.copy_context().run, args=(discover,), daemon=True, name=f"chat-mcp-{run.stream_id[:8]}").start()
583
+ while not done.wait(0.25):
584
+ if run.cancel.is_set() or run.ctx.cancelled:
585
+ return
586
+
587
+
588
+ def _profile_fallback_chain() -> list[dict] | None:
589
+ """The profile's fallback routes (``fallback_providers`` first, then legacy ``fallback_model``, deduplicated)
590
+ through the Agent's own resolver, like the CLI and gateway. Runs under the call's ``scoped_home``."""
591
+ from hermes_cli.config import load_config
592
+ from hermes_cli.fallback_config import get_fallback_chain
593
+
594
+ return get_fallback_chain(load_config() or {}) or None
595
+
596
+
597
+ def _main_model_request_overrides(model: str, provider) -> dict | None:
598
+ """Predecessor ``_main_model_request_overrides``: the profile's ``model.service_tier`` and ``model.extra_body``
599
+ (written by ``/api/model/set``) apply only to turns on that configured main model. Runs under ``scoped_home``."""
600
+ from hermes_cli.config import load_config
601
+
602
+ model_cfg = (load_config() or {}).get("model")
603
+ if not isinstance(model_cfg, dict):
604
+ return None
605
+ main_model = str(model_cfg.get("default") or model_cfg.get("name") or "").strip()
606
+ main_provider = str(model_cfg.get("provider") or "").strip().lower()
607
+ if not main_model or main_model != model.strip():
608
+ return None
609
+ # An unset or ``auto`` main provider resolves at runtime, so the model id alone identifies the main model.
610
+ if main_provider not in ("", "auto") and main_provider != str(provider or "").strip().lower():
611
+ return None
612
+ overrides: dict = {}
613
+ if str(model_cfg.get("service_tier") or "").strip().lower() == "priority":
614
+ overrides["service_tier"] = "priority"
615
+ extra_body = model_cfg.get("extra_body")
616
+ if isinstance(extra_body, dict) and extra_body:
617
+ overrides["extra_body"] = extra_body
618
+ return overrides or None
619
+
620
+
418
621
  def _agent_signature(model: str, provider, runtime: dict, toolsets, home: str, kwargs: dict) -> str:
419
622
  """Cache identity of an ``AIAgent``: everything its constructor bound from the resolved runtime, so a rotated key,
420
623
  a different API mode, ACP command, or credential pool never reuses an agent built for the old bundle. The key
@@ -430,6 +633,9 @@ def _agent_signature(model: str, provider, runtime: dict, toolsets, home: str, k
430
633
  "max_iterations": kwargs.get("max_iterations"), "max_tokens": kwargs.get("max_tokens"),
431
634
  # Bound at construction too: a reasoning-effort change from the composer must build a fresh agent.
432
635
  "reasoning_config": kwargs.get("reasoning_config"),
636
+ "request_overrides": kwargs.get("request_overrides"),
637
+ # Entries may carry their own ``api_key``.
638
+ "fallback_model": hashlib.sha256(json.dumps(kwargs.get("fallback_model"), sort_keys=True, default=str).encode("utf-8")).hexdigest()[:16],
433
639
  }
434
640
  return json.dumps(bundle, sort_keys=True, default=str)
435
641
 
@@ -440,19 +646,164 @@ def _evict_idle_agents_locked() -> None:
440
646
  return
441
647
  with _RUNS_LOCK:
442
648
  live = {sid for sid, sid_stream in _RUNS_BY_SESSION.items() if (r := _RUNS.get(sid_stream)) is not None and not r.finished.is_set()}
649
+ dropped = []
443
650
  for sid in list(_AGENT_CACHE):
444
651
  if len(_AGENT_CACHE) <= _AGENT_CACHE_MAX:
445
652
  break
446
653
  if sid not in live:
447
- _AGENT_CACHE.pop(sid, None)
654
+ dropped.append(_AGENT_CACHE.pop(sid))
655
+ _release_agents_locked(dropped)
656
+
657
+
658
+ def _release_agent(agent, home) -> threading.Thread:
659
+ """The gateway's soft eviction on a daemon thread under the owning profile: end the memory session with the
660
+ transcript (``on_session_end``, then provider shutdown), then release the LLM clients (the Codex app-server child,
661
+ httpx pools) while session tool state stays for a resumed session."""
662
+
663
+ def call(name: str, *args) -> bool:
664
+ method = getattr(agent, name, None)
665
+ if not callable(method):
666
+ return True
667
+ try:
668
+ method(*args)
669
+ except Exception: # noqa: BLE001 - one failed phase never skips the next
670
+ log.warning("evicted agent %s() failed", name, exc_info=True)
671
+ return False
672
+ return True
673
+
674
+ def release() -> None:
675
+ released = False
676
+ try:
677
+ with scoped_home(home):
678
+ messages = getattr(agent, "_session_messages", None)
679
+ flushed = call("shutdown_memory_provider", messages if isinstance(messages, list) else None)
680
+ released = True
681
+ thread.failed = not (call("release_clients") and flushed)
682
+ except Exception: # noqa: BLE001 - the profile scope could not be entered
683
+ log.warning("releasing an evicted agent under its profile failed", exc_info=True)
684
+ thread.failed = True
685
+ finally:
686
+ if not released:
687
+ # The memory flush needs the profile; the LLM clients and the Codex child close regardless.
688
+ call("release_clients")
689
+ _release_session_db(getattr(agent, "_talaria_session_db", None))
690
+ with _RELEASES_LOCK:
691
+ _RELEASES.pop(thread, None)
692
+
693
+ thread = threading.Thread(target=release, daemon=True, name="chat-agent-release")
694
+ thread.failed = False # type: ignore[attr-defined] - read by profile deletion
695
+ with _RELEASES_LOCK:
696
+ _RELEASES[thread] = home
697
+ thread.start()
698
+ return thread
699
+
700
+
701
+ def _release_agents_locked(entries) -> list[threading.Thread]:
702
+ """Release cache entries that just left the cache (caller holds ``_AGENT_CACHE_LOCK``). An agent a turn still holds
703
+ keeps its memory provider and clients; that turn releases it when it ends."""
704
+ with _RUNS_LOCK:
705
+ held = [run.agent for run in _RUNS.values()] + _COMMITTING
706
+ return [_release_agent(agent, home) for agent, _signature, home in entries if not any(agent is other for other in held)]
707
+
708
+
709
+ def _release_if_dropped_locked(agent, home) -> None:
710
+ """A holder (turn or commit) is done with ``agent``: release it if it left the cache meanwhile and nothing else holds
711
+ it (caller holds ``_AGENT_CACHE_LOCK``)."""
712
+ if agent is not None and not any(entry[0] is agent for entry in _AGENT_CACHE.values()):
713
+ _release_agents_locked([(agent, None, home)])
714
+
715
+
716
+ def _join(threads, deadline: float) -> None:
717
+ for thread in threads:
718
+ thread.join(max(0.0, deadline - time.monotonic()))
448
719
 
449
720
 
450
721
  def evict_all_agents() -> int:
451
722
  """Drop every cached agent (credentials or environment changed underneath them)."""
452
723
  with _AGENT_CACHE_LOCK:
453
- count = len(_AGENT_CACHE)
724
+ entries = list(_AGENT_CACHE.values())
454
725
  _AGENT_CACHE.clear()
455
- return count
726
+ _release_agents_locked(entries)
727
+ return len(entries)
728
+
729
+
730
+ def drain_agents(timeout: float = _AGENT_RELEASE_TIMEOUT) -> None:
731
+ """Shutdown: release every cached agent, stop running turns (like a Stop) so each releases the agent it holds, and
732
+ wait, bounded, for those turns and every release still running."""
733
+ deadline = time.monotonic() + timeout
734
+ evict_all_agents()
735
+ with _RUNS_LOCK:
736
+ _DRAINING.set()
737
+ runs = list(_RUNS.values())
738
+ for run in runs:
739
+ run.cancel.set()
740
+ while time.monotonic() < deadline:
741
+ with _AGENT_CACHE_LOCK, _RUNS_LOCK:
742
+ if not _COMMITTING and not any(_RUNS.get(run.stream_id) is run for run in runs):
743
+ break
744
+ time.sleep(0.05)
745
+ # A turn may have cached its agent after the first eviction.
746
+ evict_all_agents()
747
+ with _RELEASES_LOCK:
748
+ threads = list(_RELEASES)
749
+ _join(threads, deadline)
750
+
751
+
752
+ def release_profile_agents(home) -> None:
753
+ """Profile deletion: release the profile's cached agents and wait for every release running under its home. Fails
754
+ rather than let the home go while a memory flush still runs under it."""
755
+ target = os.path.realpath(home)
756
+ with _AGENT_CACHE_LOCK:
757
+ entries = [_AGENT_CACHE.pop(sid) for sid, entry in list(_AGENT_CACHE.items()) if os.path.realpath(entry[2]) == target]
758
+ _release_agents_locked(entries)
759
+ with _RELEASES_LOCK:
760
+ threads = [thread for thread, owner in _RELEASES.items() if os.path.realpath(owner) == target]
761
+ _join(threads, time.monotonic() + _PROFILE_RELEASE_TIMEOUT)
762
+ if any(thread.is_alive() for thread in threads):
763
+ raise RpcError("The profile is still saving memory from its chats. Retry in a moment.")
764
+ if any(thread.failed for thread in threads):
765
+ raise RpcError("Memory from the profile's chats could not be saved; see the sidecar log. Retry to delete the profile anyway.")
766
+
767
+
768
+ def _acquire_session_db(home):
769
+ """TAL-529: the profile's ``state.db`` from the Agent's shared-handle registry, one reference per agent (released with
770
+ it), so the turn's conversation reaches ``hermes sessions``, CLI resume, and ``session_search``. None when it cannot
771
+ open: the turn runs without it, like the predecessor's ``_build_session_db_for_stream``."""
772
+ try:
773
+ from hermes_state_registry import acquire
774
+
775
+ return acquire(Path(home) / "state.db")
776
+ except Exception: # noqa: BLE001
777
+ log.warning("state.db unavailable under %s; this agent's turns will not reach it", home, exc_info=True)
778
+ return None
779
+
780
+
781
+ def _release_session_db(db) -> None:
782
+ if db is None:
783
+ return
784
+ try:
785
+ from hermes_state_registry import release_or_close
786
+
787
+ release_or_close(db)
788
+ except Exception: # noqa: BLE001
789
+ log.warning("releasing an agent's state.db handle failed", exc_info=True)
790
+
791
+
792
+ def _execution_session_id(db, session_id: str) -> str:
793
+ """Predecessor ``_resolve_agent_execution_session_id``: a new agent for a session the Agent rotated by compression
794
+ (the old agent went with a sidecar restart or a runtime change) runs on the live tip, with the server's history,
795
+ rather than the closed Web id. The tip qualifies as in the Agent's own adoption: a different, still-open row. Any
796
+ doubt keeps the Web id."""
797
+ try:
798
+ tip = db.get_compression_tip(session_id)
799
+ if tip and tip != session_id:
800
+ row = db.get_session(tip)
801
+ if row and row.get("ended_at") is None:
802
+ log.info("agent for %s runs on its live compression tip %s", session_id, tip)
803
+ return str(tip)
804
+ except Exception: # noqa: BLE001
805
+ log.warning("compression tip lookup failed for %s", session_id, exc_info=True)
806
+ return session_id
456
807
 
457
808
 
458
809
  def _agent_class():
@@ -463,6 +814,15 @@ def _agent_class():
463
814
  return AIAgent
464
815
 
465
816
 
817
+ def _max_iterations_summary_request() -> str:
818
+ """The user row the Agent appends to ask for a summary once its tool budget runs out; the server drops it."""
819
+ try:
820
+ from agent.context_compressor import MAX_ITERATIONS_SUMMARY_REQUEST
821
+ except Exception: # noqa: BLE001
822
+ return ""
823
+ return str(MAX_ITERATIONS_SUMMARY_REQUEST)
824
+
825
+
466
826
  def _supported(cls, name: str) -> bool:
467
827
  """Whether ``cls.__init__`` accepts ``name`` (named, or through ``**kwargs``)."""
468
828
  try:
@@ -472,6 +832,64 @@ def _supported(cls, name: str) -> bool:
472
832
  return name in parameters or any(p.kind is inspect.Parameter.VAR_KEYWORD for p in parameters.values())
473
833
 
474
834
 
835
+ def _approval_notifier(session_id: str, emit):
836
+ """The Agent's gateway notify callback for one turn: emits each parked approval, then ``approval_resolved``
837
+ (id plus the Agent's reason) once its wait ends by any path, an ``approvals.timeout`` expiry included (TAL-514)."""
838
+ try:
839
+ from tools.approval import register_gateway_settle
840
+ except Exception: # noqa: BLE001
841
+ register_gateway_settle = None
842
+
843
+ def notify(data):
844
+ payload = dict(data or {})
845
+ payload.setdefault("session_id", session_id)
846
+ payload.setdefault("approval_id", str(payload.get("request_id") or uuid.uuid4().hex))
847
+ emit("approval", payload)
848
+ request_id = str(payload.get("request_id") or "")
849
+ if register_gateway_settle is None or not request_id:
850
+ return
851
+ approval_id = payload["approval_id"]
852
+
853
+ def settle(reason):
854
+ emit("approval_resolved", {"approval_id": approval_id, "session_id": session_id, "reason": str(reason or "")})
855
+
856
+ # False: the wait already ended between the emit and this registration.
857
+ if not register_gateway_settle(session_id, request_id, settle):
858
+ settle("resolved")
859
+
860
+ return notify
861
+
862
+
863
+ #: The Agent's compression-start lifecycle lines: "Compacting context" (and its heartbeat), pre-API, 413, and too-large
864
+ #: compression. Preflight and idle notices are left out: "Compacting context" follows them once compression proceeds.
865
+ _COMPRESSION_START_MARKERS = ("compacting context", "pre-api compression:", "context too large", "— compressing (", "- compressing (", "compression attempt")
866
+ _COMPRESSION_NOT_STARTED = ("skipping", "defer", "cooldown", "will not start")
867
+
868
+
869
+ def _is_compression_start(lower: str) -> bool:
870
+ """Predecessor ``_is_agent_compression_start_status``: only real start notices, never skips or post-compress chatter."""
871
+ if any(marker in lower for marker in _COMPRESSION_NOT_STARTED):
872
+ return False
873
+ if "compressed" in lower and "compressing" not in lower and "compression attempt" not in lower:
874
+ return False
875
+ return any(marker in lower for marker in _COMPRESSION_START_MARKERS)
876
+
877
+
878
+ def _status_events(kind, text: str) -> list[str]:
879
+ """Events for one Agent status line: the compressing card, a fallback warning, and the turn's terminal error."""
880
+ lower = text.lower()
881
+ events = ["terminal_error"] if "non-retryable error" in lower else []
882
+ if str(kind or "").strip().lower() == "lifecycle":
883
+ if _is_compression_start(lower):
884
+ return events + ["compressing"]
885
+ # The Agent's own compression retries ("Compressed N → M messages, retrying...") are not provider fallbacks.
886
+ if "compress" in lower or "context reduced to" in lower:
887
+ return events
888
+ if "fallback" in lower or "rate limit" in lower or ("retry" in lower and "non-retryable" not in lower):
889
+ events.append("warning")
890
+ return events
891
+
892
+
475
893
  def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn, one function
476
894
  session_id = str(params.get("session_id") or "").strip()
477
895
  stream_id = str(params.get("stream_id") or "").strip()
@@ -487,10 +905,13 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
487
905
  history = []
488
906
  if not isinstance(history, list):
489
907
  raise InvalidParams("conversation_history must be a list")
490
- toolsets = params.get("enabled_toolsets")
908
+ toolsets = _turn_toolsets(params.get("enabled_toolsets"))
491
909
  system_message = params.get("system_message")
910
+ home = profile_home_param(params)
492
911
  run = _Run(stream_id, session_id, ctx)
493
912
  with _RUNS_LOCK:
913
+ if _DRAINING.is_set():
914
+ raise RpcError("The sidecar is shutting down.", condition="sidecar_unavailable")
494
915
  prior = _RUNS.get(_RUNS_BY_SESSION.get(session_id) or "")
495
916
  # A previous turn for this session that never returned (a tool ignoring its interrupt) still owns the cached
496
917
  # agent; this turn must not share, rebind, or un-interrupt that live instance.
@@ -499,17 +920,24 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
499
920
  _RUNS_BY_SESSION[session_id] = stream_id
500
921
  raw_emit = ctx.emit
501
922
  steer_state = {"last": None}
923
+ usage_state = {"last": None}
502
924
 
503
925
  def emit(event, data=None):
504
926
  """Predecessor ``_webui_steer_events_before``: before content frames, report the Agent's pending steer text
505
- whenever it changed so the server can mark consumed steers live rather than only after ``done``."""
927
+ whenever it changed so the server can mark consumed steers live rather than only after ``done``. TAL-397: the
928
+ Agent's session counters ride the same boundary, so the server meters cost and tokens while the turn runs."""
506
929
  if event in ("token", "reasoning", "interim_assistant", "tool", "tool_complete") and run.agent is not None:
507
930
  pending = _agent_pending_steer_text(run.agent)
508
931
  if pending != steer_state["last"]:
509
932
  steer_state["last"] = pending
510
933
  raw_emit("steer_pending", {"text": pending})
934
+ usage = _usage(run.agent)
935
+ if usage != usage_state["last"]:
936
+ usage_state["last"] = usage
937
+ raw_emit("usage", usage)
511
938
  raw_emit(event, data)
512
939
  try:
940
+ _discover_mcp_tools(run)
513
941
  runtime = _resolve_runtime(provider, model)
514
942
  resolved_model = model or str(runtime.get("model") or "")
515
943
  resolved_provider = provider or runtime.get("provider")
@@ -560,6 +988,13 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
560
988
  call["snippet"] = snippet
561
989
  break
562
990
  payload = {"event_type": "tool.completed", "name": name, "preview": snippet, "args": _args_snapshot(args), "tid": tid, "raw_result": _raw_result(function_result)}
991
+ # TAL-397: ``raw_result`` caps nested values, so the todo tool's list travels whole for the server's ``todo_state``.
992
+ if name == "todo" and function_result is not None:
993
+ payload["todo_result"] = function_result if isinstance(function_result, str) else json.dumps(function_result, default=str)
994
+ # TAL-448: likewise a result's unified ``diff``, so the server's ``edit_diff`` counts cover the whole change.
995
+ diff = _result_diff(function_result)
996
+ if diff is not None:
997
+ payload["result_diff"] = diff
563
998
  cost = _delegation_cost_usd(name, function_result)
564
999
  if cost is not None:
565
1000
  payload["cost_usd"] = cost
@@ -578,15 +1013,13 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
578
1013
 
579
1014
  def on_status(kind, message):
580
1015
  text = str(message or "").strip()
581
- if not text:
582
- return
583
- lower = text.lower()
584
- if "compress" in lower and ("start" in lower or "compressing" in lower):
585
- emit("compressing", {"session_id": session_id, "message": "Compressing context"})
586
- elif "fallback" in lower or "rate limit" in lower or "retry" in lower:
587
- emit("warning", {"type": "fallback", "message": text})
588
- elif "non-retryable error" in lower:
589
- emit("status", {"kind": "terminal_error", "message": text})
1016
+ for event in _status_events(kind, text):
1017
+ if event == "compressing":
1018
+ emit("compressing", {"session_id": session_id, "message": "Compressing context"})
1019
+ elif event == "warning":
1020
+ emit("warning", {"type": "fallback", "message": text})
1021
+ else:
1022
+ emit("status", {"kind": "terminal_error", "message": text})
590
1023
 
591
1024
  def clarify_callback(question, choices, multi_select=False, questions=None):
592
1025
  choices_list = [str(c) for c in (choices or [])]
@@ -626,15 +1059,7 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
626
1059
  from tools.approval import register_gateway_notify, unregister_gateway_notify
627
1060
  except Exception: # noqa: BLE001
628
1061
  register_gateway_notify = unregister_gateway_notify = None
629
- if register_gateway_notify is not None:
630
- def _approval_cb(data):
631
- payload = dict(data or {})
632
- payload.setdefault("session_id", session_id)
633
- payload.setdefault("approval_id", str(payload.get("request_id") or uuid.uuid4().hex))
634
- emit("approval", payload)
635
- approval_cb = _approval_cb
636
- else:
637
- approval_cb = None
1062
+ approval_cb = _approval_notifier(session_id, emit) if register_gateway_notify is not None else None
638
1063
 
639
1064
  kwargs: dict = dict(
640
1065
  model=resolved_model,
@@ -643,7 +1068,7 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
643
1068
  api_key=runtime.get("api_key"),
644
1069
  platform="webui",
645
1070
  quiet_mode=True,
646
- enabled_toolsets=toolsets if isinstance(toolsets, list) else None,
1071
+ enabled_toolsets=toolsets,
647
1072
  session_id=session_id,
648
1073
  stream_delta_callback=on_token,
649
1074
  reasoning_callback=on_reasoning,
@@ -671,23 +1096,48 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
671
1096
  reasoning_config = params.get("reasoning_config")
672
1097
  if isinstance(reasoning_config, dict) and _supported(AIAgent, "reasoning_config"):
673
1098
  kwargs["reasoning_config"] = reasoning_config
1099
+ fallback_chain = _profile_fallback_chain()
1100
+ if fallback_chain and _supported(AIAgent, "fallback_model"):
1101
+ kwargs["fallback_model"] = fallback_chain
1102
+ request_overrides = _main_model_request_overrides(resolved_model, resolved_provider)
1103
+ if request_overrides and _supported(AIAgent, "request_overrides"):
1104
+ kwargs["request_overrides"] = request_overrides
674
1105
  signature = _agent_signature(resolved_model, resolved_provider, runtime, toolsets, str(params.get("profile_home")), kwargs)
675
1106
  agent = None
1107
+
1108
+ def bind(bound) -> None:
1109
+ """Claim the agent for this run under ``_AGENT_CACHE_LOCK``, so an eviction never releases it mid-turn."""
1110
+ run.prior_messages = getattr(bound, "_session_messages", None)
1111
+ run.agent = bound
1112
+
676
1113
  if not session_busy:
677
1114
  with _AGENT_CACHE_LOCK:
678
1115
  cached = _AGENT_CACHE.get(session_id)
679
1116
  if cached and cached[1] == signature:
680
1117
  agent = cached[0]
681
1118
  _AGENT_CACHE.move_to_end(session_id)
1119
+ bind(agent)
682
1120
  if agent is not None:
683
1121
  for name in ("stream_delta_callback", "reasoning_callback", "tool_progress_callback", "clarify_callback", "interim_assistant_callback", "tool_start_callback", "tool_complete_callback", "status_callback"):
684
1122
  if name in kwargs and hasattr(agent, name):
685
1123
  setattr(agent, name, kwargs[name])
686
1124
  else:
687
- agent = AIAgent(**kwargs)
1125
+ # A ``/btw`` side question's throwaway session stays out of state.db.
1126
+ session_db = _acquire_session_db(home) if not params.get("ephemeral") and _supported(AIAgent, "session_db") else None
1127
+ if session_db is not None:
1128
+ kwargs["session_db"] = session_db
1129
+ kwargs["session_id"] = _execution_session_id(session_db, session_id)
1130
+ try:
1131
+ agent = AIAgent(**kwargs)
1132
+ except BaseException:
1133
+ _release_session_db(session_db)
1134
+ raise
1135
+ agent._talaria_session_db = session_db # released with the agent (``_release_agent``)
688
1136
  with _AGENT_CACHE_LOCK:
689
- _AGENT_CACHE[session_id] = (agent, signature)
690
- _AGENT_CACHE.move_to_end(session_id)
1137
+ bind(agent)
1138
+ replaced = _AGENT_CACHE.pop(session_id, None)
1139
+ _AGENT_CACHE[session_id] = (agent, signature, home)
1140
+ _release_agents_locked([replaced] if replaced else [])
691
1141
  _evict_idle_agents_locked()
692
1142
  # Predecessor ``agent.ephemeral_system_prompt``: personality, surface context, progress guidance, and delivery
693
1143
  # hints travel as runtime instructions that are never persisted to history.
@@ -701,8 +1151,7 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
701
1151
  with _APPROVAL_CB_LOCK:
702
1152
  register_gateway_notify(session_id, approval_cb)
703
1153
  _APPROVAL_CB_OWNER[session_id] = stream_id
704
- run.prior_messages = getattr(agent, "_session_messages", None)
705
- run.agent = agent
1154
+ usage_state["last"] = _usage(agent)
706
1155
  compressions_before = int(getattr(getattr(agent, "context_compressor", None), "compression_count", 0) or 0)
707
1156
 
708
1157
  # A cached agent may still carry the interrupt a previous cancel left behind; the Agent keeps a pending
@@ -742,8 +1191,12 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
742
1191
  run_params = set()
743
1192
  if system_message and ("system_message" in run_params or not run_params):
744
1193
  run_kwargs["system_message"] = system_message
745
- if "persist_user_message" in run_params and isinstance(user_message, str):
746
- run_kwargs["persist_user_message"] = user_message
1194
+ # The server's clean user text (no workspace prefix) is what state.db stores; the Agent keeps the sent bytes beside it.
1195
+ persist = params.get("persist_user_message")
1196
+ if not (isinstance(persist, str) and persist.strip()):
1197
+ persist = user_message if isinstance(user_message, str) else None
1198
+ if "persist_user_message" in run_params and persist is not None:
1199
+ run_kwargs["persist_user_message"] = persist
747
1200
  error: str | None = None
748
1201
  result: dict = {}
749
1202
  try:
@@ -772,30 +1225,48 @@ def start(ctx: CallContext, params: dict) -> dict: # noqa: PLR0915 - one turn,
772
1225
  error = str(last_error)
773
1226
  if not error and isinstance(result.get("error"), str) and result.get("error"):
774
1227
  error = str(result["error"])
775
- status = "cancelled" if cancelled else ("error" if error and not result.get("messages") else "completed")
1228
+ # The Agent ends a failed turn with ``failed``/``partial`` (never ``status``), often after text streamed and with
1229
+ # the turn's messages; a failed turn is an error whatever it carries.
1230
+ failed = bool(result.get("failed"))
1231
+ partial = bool(result.get("partial"))
1232
+ if not error and failed and result.get("final_response"):
1233
+ error = str(result["final_response"])
1234
+ status = "cancelled" if cancelled else ("error" if failed or (error and not result.get("messages")) else "completed")
776
1235
  pending_steer = str(result.get("pending_steer") or "") if isinstance(result, dict) else ""
1236
+ messages = [m for m in (result.get("messages") or []) if isinstance(m, dict)]
1237
+ compressed = compressions_after > compressions_before
1238
+ context_messages, context_estimate = _post_compression_context(agent, messages, system_message) if compressed and messages and status == "completed" else (None, None)
777
1239
  return {
778
1240
  "status": status,
779
- "messages": [m for m in (result.get("messages") or []) if isinstance(m, dict)],
1241
+ "messages": messages,
1242
+ "context_messages": context_messages,
1243
+ "post_compression_context_tokens_estimate": context_estimate,
780
1244
  "final_response": str(result.get("final_response") or ""),
781
1245
  "error": error,
782
- "result_status": str(result.get("status") or result.get("state") or ""),
783
- "tool_limit_reached": bool(result.get("tool_limit_reached") or result.get("max_iterations_reached")),
1246
+ "failed": failed,
1247
+ "partial": partial,
1248
+ "compression_exhausted": bool(result.get("compression_exhausted")),
1249
+ # The pinned Agent marks an exhausted tool budget only in ``turn_exit_reason`` (``max_iterations_reached(n/n)``).
1250
+ "tool_limit_reached": bool(result.get("tool_limit_reached") or result.get("max_iterations_reached") or str(result.get("turn_exit_reason") or "").startswith("max_iterations_reached")),
1251
+ "max_iterations_summary_request": _max_iterations_summary_request(),
784
1252
  "usage": _usage(agent),
785
1253
  "context": _context_length(agent),
786
1254
  "model": str(getattr(agent, "model", None) or resolved_model),
787
1255
  "provider": str(resolved_provider or ""),
788
- "compressed": compressions_after > compressions_before,
1256
+ "compressed": compressed,
789
1257
  "agent_session_id": str(getattr(agent, "session_id", None) or session_id),
790
1258
  "token_sent": token_sent[0],
791
1259
  "pending_steer": pending_steer,
792
1260
  "live_tool_calls": live_tool_calls,
793
1261
  }
794
1262
  finally:
795
- with _RUNS_LOCK:
796
- _RUNS.pop(stream_id, None)
797
- if _RUNS_BY_SESSION.get(session_id) == stream_id:
798
- _RUNS_BY_SESSION.pop(session_id, None)
1263
+ with _AGENT_CACHE_LOCK:
1264
+ with _RUNS_LOCK:
1265
+ _RUNS.pop(stream_id, None)
1266
+ if _RUNS_BY_SESSION.get(session_id) == stream_id:
1267
+ _RUNS_BY_SESSION.pop(session_id, None)
1268
+ # An agent evicted or replaced while this turn held it is released now that the turn is over.
1269
+ _release_if_dropped_locked(run.agent, home)
799
1270
 
800
1271
 
801
1272
  def _checkpoint_required() -> bool:
@@ -861,7 +1332,7 @@ def compress(ctx: CallContext, params: dict) -> dict:
861
1332
  model = str(params.get("model") or "").strip()
862
1333
  provider = str(params.get("model_provider") or "").strip() or None
863
1334
  focus_topic = str(params.get("focus_topic") or "").strip()[:500] or None
864
- toolsets = params.get("enabled_toolsets")
1335
+ toolsets = _turn_toolsets(params.get("enabled_toolsets"))
865
1336
  runtime = _resolve_runtime(provider, model)
866
1337
  if not runtime.get("api_key"):
867
1338
  raise RpcError("No provider configured -- cannot compress.", condition="credential_missing")
@@ -875,7 +1346,7 @@ def compress(ctx: CallContext, params: dict) -> dict:
875
1346
  api_key=runtime.get("api_key"),
876
1347
  platform="webui",
877
1348
  quiet_mode=True,
878
- enabled_toolsets=toolsets if isinstance(toolsets, list) else None,
1349
+ enabled_toolsets=toolsets,
879
1350
  session_id=session_id,
880
1351
  )
881
1352
  for name, value in (
@@ -1017,33 +1488,50 @@ def register(registry) -> None:
1017
1488
 
1018
1489
  @registry.method("chat.evict_agent", requires_agent=False)
1019
1490
  def evict_(ctx: CallContext, params: dict) -> dict:
1491
+ """Drop a session's cached agent. Only delete/clear (``clear_session``) end its approval grants, parked
1492
+ approvals and kernels; a model switch, truncate or compress leaves a running turn's agent in place."""
1020
1493
  session_id = str(params.get("session_id") or "").strip()
1494
+ end_session = params.get("clear_session") is True
1021
1495
  with _AGENT_CACHE_LOCK:
1022
- evicted = _AGENT_CACHE.pop(session_id, None) is not None
1023
- try:
1024
- from tools.approval import clear_session
1025
- clear_session(session_id)
1026
- except Exception: # noqa: BLE001
1027
- pass
1028
- return {"evicted": evicted}
1496
+ with _RUNS_LOCK:
1497
+ run = _RUNS.get(_RUNS_BY_SESSION.get(session_id) or "")
1498
+ if not end_session and run is not None and not run.finished.is_set():
1499
+ return {"evicted": False}
1500
+ entry = _AGENT_CACHE.pop(session_id, None)
1501
+ _release_agents_locked([entry] if entry else [])
1502
+ if end_session:
1503
+ try:
1504
+ from tools.approval import clear_session
1505
+ clear_session(session_id)
1506
+ except Exception: # noqa: BLE001
1507
+ pass
1508
+ return {"evicted": entry is not None}
1029
1509
 
1030
1510
  @registry.method("chat.commit_memory", requires_agent=False)
1031
1511
  def commit_memory_(ctx: CallContext, params: dict) -> dict:
1032
- """Predecessor ``commit_session_memory``: flush the cached Agent's memory for a session the user just left."""
1512
+ """Predecessor ``commit_session_memory``: flush the cached Agent's memory for a session the user just left.
1513
+ Providers get the session's transcript and run under its profile's ``scoped_home``."""
1033
1514
  session_id = str(params.get("session_id") or "").strip()
1515
+ home = profile_home_param(params)
1034
1516
  with _AGENT_CACHE_LOCK:
1035
1517
  cached = _AGENT_CACHE.get(session_id)
1036
- with _RUNS_LOCK:
1037
- busy = (_RUNS_BY_SESSION.get(session_id) or "") in _RUNS
1038
- agent = cached[0] if cached else None
1039
- commit = getattr(agent, "commit_memory_session", None) if agent is not None else None
1040
- if commit is None or busy:
1041
- return {"committed": False}
1518
+ with _RUNS_LOCK:
1519
+ busy = _DRAINING.is_set() or (_RUNS_BY_SESSION.get(session_id) or "") in _RUNS
1520
+ agent = cached[0] if cached else None
1521
+ commit = getattr(agent, "commit_memory_session", None) if agent is not None else None
1522
+ if commit is None or busy:
1523
+ return {"committed": False}
1524
+ _COMMITTING.append(agent)
1042
1525
  try:
1043
- commit()
1526
+ with scoped_home(home):
1527
+ commit(list(getattr(agent, "_session_messages", None) or []))
1044
1528
  except Exception as exc: # noqa: BLE001
1045
1529
  log.warning("commit_memory_session() failed for session %s: %s", session_id, exc)
1046
1530
  return {"committed": False}
1531
+ finally:
1532
+ with _AGENT_CACHE_LOCK:
1533
+ _COMMITTING.remove(agent)
1534
+ _release_if_dropped_locked(agent, cached[2])
1047
1535
  return {"committed": True}
1048
1536
 
1049
1537
  @registry.method("approval.respond", requires_agent=True)
@@ -1101,7 +1589,9 @@ def register(registry) -> None:
1101
1589
  except Exception as exc: # noqa: BLE001
1102
1590
  raise RpcError(f"approval module unavailable: {exc}", condition="agent_unavailable") from exc
1103
1591
  (enable_session_yolo if enabled else disable_session_yolo)(session_id)
1104
- released = resolve_gateway_approval(session_id, "session", resolve_all=True) if enabled else 0
1592
+ # Parked approvals are released `once`: a `session` or `always` grant would keep their patterns approved after
1593
+ # YOLO turns off, whatever choice the card that enabled it carried.
1594
+ released = resolve_gateway_approval(session_id, "once", resolve_all=True) if enabled else 0
1105
1595
  return {"yolo_enabled": bool(is_session_yolo_enabled(session_id)), "released": int(released or 0)}
1106
1596
 
1107
1597
  @registry.method("clarify.respond", requires_agent=False)