@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
@@ -12,29 +12,57 @@ import { isWithin, resolvePathLikePython } from '../workspace/paths.js';
12
12
  import { SidecarError } from '../sidecar/client.js';
13
13
  import { HttpFailure, markSessionTitleGenerated } from './service.js';
14
14
  import { StreamRegistry, SessionChannels } from './streams.js';
15
+ import { ChatUsageSchema } from '@maudecode/talaria-web-contracts';
15
16
  import { PendingPrompts, clarifyReply } from './pending.js';
16
17
  import { RunJournal } from './journal.js';
17
- import { CONTEXT_USAGE_FIELDS, Session, titleFrom } from './session.js';
18
- import { buildActiveTurnToken, completedToolIndex, publicToolFrame, redactSessionData, redactString, withToolId } from '../redact.js';
18
+ import { CONTEXT_USAGE_FIELDS, Session, contextUsage, titleFrom } from './session.js';
19
+ import { anchorMessageKey, compressionReference, markerSummary, visibleMessagesForAnchor } from './compress.js';
20
+ import { buildActiveTurnToken, completedToolIndex, publicToolFrame, redactNestedMessageContainers, redactSessionData, redactString, withToolId } from '../redact.js';
19
21
  import { dict } from '../config/agent-config.js';
20
22
  import { ReasoningTitleTracker, reasoningEventPayload } from './reasoning-titles.js';
21
23
  import { messageWindowForDisplay, messagesForLimitedPayload, toolCallsForMessageWindow } from './window.js';
22
- import { attachTodoState } from './todo.js';
24
+ import { UNSETTLED_TODO_KEY, attachTodoState, parseTodoToolResult } from './todo.js';
23
25
  import { withSessionWireFlags } from './list.js';
24
26
  import { hydrateAnchorActivityScenes, turnTerminalState, withTurnIds } from './anchor.js';
25
27
  import { persistentStateChanges, persistentStateSnapshot } from './state-saved.js';
26
28
  import { maxIterationsFromConfig, maxTokensFromConfig, processWakeupMaxIterations, reasoningConfigFromConfig, webuiEphemeralSystemPrompt, workspaceSystemMessage } from './turn-context.js';
27
- import { agentSteerText, assistantReplyAddedAfterCurrentTurn, buildPartialMessage, escapeWorkspacePrefixPath, checkpointTurnStart, extractToolCallsFromMessages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, sessionLacksFinalAssistantAnswer, splitThinkingFromContent, stoppedTurnContext, stripXmlToolCalls, toolOutcome, withAttachmentObjects, withBodyExcerpts, withToolCallOutcomes, workspaceContextPrefix } from './merge.js';
29
+ import { agentSteerText, assistantReplyAddedAfterCurrentTurn, attachedFilesPrompt, buildPartialMessage, dedupeContext, checkpointTurnStart, extractToolCallsFromMessages, hasNativeImages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, sessionLacksFinalAssistantAnswer, splitDisplayText, stateDbSeenId, joinReasoning, reasoningFieldsText, stoppedTurnContext, toolOutcome, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withToolCallOutcomes, workspaceContextPrefix, withoutMaxIterationSummaryRequest, withNativeImagesRestored, withoutNativeImages, withoutToolImages } from './merge.js';
28
30
  import { mayBecomeSilentReply, turnOrigin, withBackgroundUpdates } from './background-updates.js';
29
31
  import { fallbackTitleFromExchange, firstExchangeSnippets, isGenericFallbackTitle, latestExchangeSnippets, looksInvalidGeneratedTitle, looksLikeDefaultCliTitle, sanitizeGeneratedTitle, titleLanguageMismatch, titlePrompts } from './titles.js';
32
+ import { WORKSPACE_BUSY_MESSAGE } from '../workspace/git.js';
30
33
  import { str } from '../util.js';
34
+ import { toolEditDiff } from './tool-display.js';
31
35
  export const CHAT_LOCK_WAIT_SECONDS = 2;
32
36
  const IMAGE_MODE_TIMEOUT_MS = 15_000;
33
37
  const TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT = 80;
38
+ /** TAL-397: streamed text reports the live meter at most this often (seconds); a counter change reports at once. */
39
+ const METERING_INTERVAL_S = 1;
34
40
  /** How long a user's message waits for a stopped background turn to unwind before its own turn is admitted. */
35
41
  const BACKGROUND_UNWIND_WAIT_MS = 30_000;
42
+ const COMPRESSION_EXHAUSTED = { label: 'Context compression exhausted', type: 'compression_exhausted', hint: 'The conversation context is too large to compress safely. Start a new conversation or retry with a narrower task.' };
43
+ /** The Agent's session counters onto a session's: a zero or missing reading keeps the figure it already has. */
44
+ function applyAgentUsage(target, usage) {
45
+ if (usage.prompt_tokens > 0)
46
+ target.input_tokens = usage.prompt_tokens;
47
+ if (usage.completion_tokens > 0)
48
+ target.output_tokens = usage.completion_tokens;
49
+ if (usage.estimated_cost_usd !== null)
50
+ target.estimated_cost = usage.estimated_cost_usd;
51
+ if (usage.cache_read_tokens > 0)
52
+ target.cache_read_tokens = usage.cache_read_tokens;
53
+ if (usage.cache_write_tokens > 0)
54
+ target.cache_write_tokens = usage.cache_write_tokens;
55
+ }
56
+ /** TAL-397: the `metering` frame's counters, the same figures the session persists. */
57
+ const meteringUsage = (c) => ({
58
+ input_tokens: c.input_tokens || 0, output_tokens: c.output_tokens || 0, estimated_cost: typeof c.estimated_cost === 'number' ? c.estimated_cost : null,
59
+ cache_read_tokens: c.cache_read_tokens || 0, cache_write_tokens: c.cache_write_tokens || 0,
60
+ });
61
+ /** `compressionExhausted`: the Agent flagged the turn itself, so its wording does not matter. */
36
62
  export function classifyProviderError(errStr, opts = {}) {
37
63
  const lower = errStr.toLowerCase();
64
+ if (opts.compressionExhausted)
65
+ return COMPRESSION_EXHAUSTED;
38
66
  if (opts.condition === 'credential_missing')
39
67
  return { label: 'Authentication failed', type: 'auth_mismatch', hint: 'The selected model may not be supported by your configured provider or your API key is invalid. Run `hermes model` in your terminal to update credentials, then restart the WebUI.' };
40
68
  const cancelled = ['cancelled by user', 'canceled by user', 'user cancelled', 'user canceled', 'task cancelled', 'task canceled', 'cancellederror'].some((k) => lower.includes(k));
@@ -60,7 +88,7 @@ export function classifyProviderError(errStr, opts = {}) {
60
88
  if (notFound)
61
89
  return { label: 'Model not found', type: 'model_not_found', hint: 'The selected model was not found by the provider. Check the model ID in Settings or run `hermes model` to verify it exists for your provider.' };
62
90
  if (compressionExhausted)
63
- return { label: 'Context compression exhausted', type: 'compression_exhausted', hint: 'The conversation context is too large to compress safely. Start a new conversation or retry with a narrower task.' };
91
+ return COMPRESSION_EXHAUSTED;
64
92
  if (opts.silentFailure)
65
93
  return { label: 'No response from provider', type: 'no_response', hint: 'The provider returned no content and no error. This often means a usage/rate limit was hit silently. Check provider status, switch providers via `hermes model`, or try again in a moment.' };
66
94
  return { label: 'Error', type: 'error', hint: '' };
@@ -132,7 +160,27 @@ export function isGenericContinuationIntent(text) {
132
160
  const parts = normalized.split(' ');
133
161
  return parts.length > 0 && parts.length <= 2 && parts.every((p) => GENERIC_CONTINUATION_INTENTS.has(p));
134
162
  }
163
+ /**
164
+ * Python `_aux_title_generation_enabled`: `auxiliary.title_generation.enabled` with the Agent's
165
+ * `is_truthy_value(default=True)` semantics. Missing/null enables; a string enables only as 1/true/yes/on.
166
+ */
167
+ export function titleGenerationEnabled(cfg) {
168
+ const value = dict(dict(cfg.auxiliary).title_generation).enabled;
169
+ if (value === undefined || value === null)
170
+ return true;
171
+ if (typeof value === 'string')
172
+ return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
173
+ // Python `bool()`: empty containers are false; any other number than zero (NaN included) is true.
174
+ if (Array.isArray(value))
175
+ return value.length > 0;
176
+ if (typeof value === 'object')
177
+ return Object.keys(value).length > 0;
178
+ if (typeof value === 'number')
179
+ return value !== 0;
180
+ return Boolean(value);
181
+ }
135
182
  /** Python `_explicit_text_signal`: `agent.image_input_mode: text` or a configured `auxiliary.vision` backend. */
183
+ const isImageAttachment = (att) => Boolean(str(att.path).trim()) && str(att.mime).trim().startsWith('image/');
136
184
  export function explicitTextSignal(cfg) {
137
185
  const agent = dict(cfg.agent);
138
186
  if (str(agent.image_input_mode ?? 'auto').trim().toLowerCase() === 'text')
@@ -157,6 +205,10 @@ export class TurnRunner {
157
205
  steerRewrites = new Map();
158
206
  deferredPendingSteer = new Map();
159
207
  lastCompletedTool = new Map();
208
+ /** TAL-397: each running turn's live counters, once the Agent reported any; an error or cancel persists them too. */
209
+ liveUsage = new Map();
210
+ /** TAL-397: each running turn's latest todo list, which an error or cancel keeps on the session. */
211
+ liveTodos = new Map();
160
212
  /** Streams whose run completed (`done` emitted) and only await title work; a late cancel is a no-op for these. */
161
213
  settledStreams = new Set();
162
214
  /**
@@ -180,6 +232,8 @@ export class TurnRunner {
180
232
  return { error: 'Web is updating. Retry after it restarts.', _status: 503 };
181
233
  if (this.deps.profileDeleting?.(s.profile ?? null))
182
234
  return { error: `Profile '${str(s.profile)}' is being deleted.`, _status: 409 };
235
+ if (this.deps.workspaceBusy?.(opts.workspace))
236
+ return { error: WORKSPACE_BUSY_MESSAGE, _status: 409 };
183
237
  const locked = s.active_stream_id;
184
238
  if (locked) {
185
239
  if (this.registry.liveIds.has(locked) || this.registry.activeRuns.has(locked))
@@ -302,13 +356,16 @@ export class TurnRunner {
302
356
  }
303
357
  const msgText = opts.msg;
304
358
  const previousMessages = structuredClone(s.messages);
305
- const previousContext = structuredClone(deps.service().modelContext(s));
306
- // Python `_sanitize_messages_for_api`: the model never sees display-only rows or a replayed cancelled prompt.
307
- const apiHistory = sanitizeMessagesForApi(previousContext);
359
+ // TAL-493: one state.db read builds the history and marks where this turn's own rows begin.
360
+ const startRead = deps.service().stateDbRead(s);
361
+ const previousContext = structuredClone(deps.service().modelContext(s, startRead.rows));
362
+ // Python `_sanitize_messages_for_agent`: the model never sees display-only rows or a replayed cancelled prompt, and
363
+ // the Agent gets back the exact bytes it sent (TAL-541).
364
+ const apiHistory = sanitizeMessagesForApi(previousContext, { preserveApiContent: true });
308
365
  const workspaceCtx = workspaceContextPrefix(opts.workspace);
309
366
  // Before the first await: a Stop can land at any point after admission.
310
367
  // An eager save already put this turn's prompt in the transcript; the Stop fallback appends it once itself.
311
- this.stopContexts.set(streamId, { previousContext: previousContext.filter((m) => m._turn_id !== streamId), historyLength: apiHistory.length, prompt: workspaceCtx + msgText, msgText, checkpointed: false });
368
+ this.stopContexts.set(streamId, { previousContext: previousContext.filter((m) => m._turn_id !== streamId), historyLength: apiHistory.length, prompt: workspaceCtx + msgText, msgText, checkpointed: false, stateDbStartId: startRead.ok ? stateDbSeenId(startRead.rows) ?? 0 : null });
312
369
  const activeTurnToken = buildActiveTurnToken(streamId, s.pending_started_at);
313
370
  const sidecar = deps.sidecar();
314
371
  const partialText = this.registry.partialText.get(streamId) ?? [];
@@ -322,6 +379,25 @@ export class TurnRunner {
322
379
  const toolStartedAt = new WeakMap();
323
380
  let tokenSent = false;
324
381
  let firstTokenAt = null;
382
+ // TAL-397: the live meter. Counters start from the session's and follow the Agent's; the rate counts text deltas.
383
+ const liveUsage = { input_tokens: s.input_tokens, output_tokens: s.output_tokens, estimated_cost: s.estimated_cost, cache_read_tokens: s.cache_read_tokens, cache_write_tokens: s.cache_write_tokens };
384
+ const meter = { deltas: 0, first: 0, last: 0, sentAt: null };
385
+ const putMetering = (force) => {
386
+ const now = deps.now();
387
+ if (!force && meter.sentAt !== null && now - meter.sentAt < METERING_INTERVAL_S)
388
+ return;
389
+ meter.sentAt = now;
390
+ const tps = meter.deltas && meter.last > meter.first ? Math.round((meter.deltas / (meter.last - meter.first)) * 10) / 10 : null;
391
+ put('metering', { session_id: sessionId, usage: meteringUsage(liveUsage), tps, tps_available: tps !== null, estimated: false });
392
+ };
393
+ const meterDelta = () => {
394
+ const now = deps.now();
395
+ if (!meter.deltas)
396
+ meter.first = now;
397
+ meter.last = now;
398
+ meter.deltas += 1;
399
+ putMetering(false);
400
+ };
325
401
  // TAL-460: a background turn's text is held back while it could still be a silence marker, so one never flashes by.
326
402
  // ponytail: checks the turn's whole text, so a marker after earlier prose streams until the settled row hides it;
327
403
  // check the last text segment instead if that shows up.
@@ -331,7 +407,12 @@ export class TurnRunner {
331
407
  let capturedTerminalError = null;
332
408
  const controller = new AbortController();
333
409
  this.abortControllers.set(streamId, controller);
334
- const userMessage = await this.buildUserMessage(workspaceCtx, msgText, opts.attachments ?? [], opts.workspace, sessionId, s, opts, controller.signal);
410
+ const attachments = opts.attachments ?? [];
411
+ // TAL-545: one image mode decides both this turn's upload and the replayed history, which a text-mode turn sends
412
+ // without native images; only a turn with an image on either side asks for it.
413
+ const imageMode = attachments.some(isImageAttachment) || hasNativeImages(apiHistory) ? await this.imageInputMode(sessionId, s, opts, controller.signal) : null;
414
+ const userMessage = this.buildUserMessage(workspaceCtx, msgText, attachments, opts.workspace, sessionId, imageMode);
415
+ const conversationHistory = imageMode === 'text' ? withoutNativeImages(apiHistory) : apiHistory;
335
416
  // The prompt the Agent actually gets, native image parts included.
336
417
  const stop = this.stopContexts.get(streamId);
337
418
  if (stop)
@@ -375,7 +456,7 @@ export class TurnRunner {
375
456
  put('context_status', { session_id: sessionId, prefill: { status: 'not_configured', source: 'none', label: '', message_count: 0 } });
376
457
  const result = await sidecar.call('chat.start', {
377
458
  profile_home: deps.profileHome(s.profile), session_id: sessionId, stream_id: streamId, workspace: opts.workspace, model: opts.model ?? '', model_provider: opts.modelProvider,
378
- user_message: userMessage, ...turnContext, conversation_history: apiHistory, enabled_toolsets: deps.toolsetsFor(s),
459
+ user_message: userMessage, ...turnContext, conversation_history: conversationHistory, enabled_toolsets: deps.toolsetsFor(s), ...(msgText ? { persist_user_message: msgText } : {}), ...(opts.ephemeral ? { ephemeral: true } : {}),
379
460
  }, {
380
461
  signal: controller.signal,
381
462
  timeoutMs: 0,
@@ -392,10 +473,12 @@ export class TurnRunner {
392
473
  partialText.push(str(data.text));
393
474
  if (holdSilence && mayBecomeSilentReply(partialText.join(''))) {
394
475
  heldText += str(data.text);
476
+ meterDelta();
395
477
  return;
396
478
  }
397
479
  put('token', { text: heldText + str(data.text) });
398
480
  heldText = '';
481
+ meterDelta();
399
482
  return;
400
483
  }
401
484
  case 'reasoning': {
@@ -404,6 +487,16 @@ export class TurnRunner {
404
487
  reasoningText.push(str(data.text));
405
488
  // Python `reasoning_event_payload`: bold or first-line titles ride on the delta they complete.
406
489
  put('reasoning', titles.prepare(reasoningEventPayload(str(data.text), reasoningText.join(''))));
490
+ meterDelta();
491
+ return;
492
+ }
493
+ case 'usage': {
494
+ const usage = ChatUsageSchema.safeParse(data);
495
+ if (usage.success) {
496
+ applyAgentUsage(liveUsage, usage.data);
497
+ this.liveUsage.set(streamId, liveUsage);
498
+ }
499
+ putMetering(true);
407
500
  return;
408
501
  }
409
502
  case 'steer_pending':
@@ -424,20 +517,36 @@ export class TurnRunner {
424
517
  return;
425
518
  }
426
519
  case 'tool_complete': {
427
- // TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server.
428
- const { raw_result: rawResult, ...complete } = data;
429
- complete.is_error = toolOutcome(rawResult).is_error;
520
+ // TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
521
+ // are its display sections, which keep the stderr and exit code the flat preview drops.
522
+ const { raw_result: rawResult, todo_result: todoResult, result_diff: resultDiff, ...complete } = data;
523
+ const outcome = toolOutcome(rawResult);
524
+ complete.is_error = outcome.is_error;
525
+ if (rawResult !== undefined)
526
+ complete.result_view = outcome.result_view;
527
+ // TAL-448: a file edit's change, counted over the whole diff the sidecar forwards; redacted with the frame.
528
+ const editDiff = toolEditDiff(data.name, { diff: resultDiff });
529
+ if (editDiff)
530
+ complete.edit_diff = editDiff;
430
531
  const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
431
532
  if (tc) {
432
533
  const startedAt = toolStartedAt.get(tc);
433
534
  if (startedAt !== undefined)
434
535
  complete.duration = Math.round(Math.max(0, deps.now() - startedAt) * 1000) / 1000;
435
- Object.assign(tc, { done: true, snippet: data.preview, is_error: complete.is_error, duration: complete.duration ?? null });
536
+ // TAL-315: the decided view too, so a failed or cancelled turn's snapshot keeps the sections shown live.
537
+ Object.assign(tc, { done: true, snippet: data.preview, is_error: complete.is_error, duration: complete.duration ?? null, ...(complete.result_view ? { result_view: complete.result_view } : {}), ...(editDiff ? { edit_diff: editDiff } : {}) });
436
538
  }
437
539
  const id = (tc && toolIds.get(tc)) || str(data.tid) || mintToolId();
438
540
  this.lastCompletedTool.set(streamId, id);
439
541
  const redacted = deps.redactEnabled();
440
542
  put('tool_complete', publicToolFrame(withToolId(complete, id), redacted), { redacted });
543
+ // TAL-397: the todo tool's result is the session's new list; the settled session derives the same one.
544
+ const todos = data.name === 'todo' ? parseTodoToolResult(todoResult) : null;
545
+ if (todos) {
546
+ todos.ts = deps.now();
547
+ this.liveTodos.set(streamId, todos);
548
+ put('todo_state', redactNestedMessageContainers({ ...todos, session_id: sessionId, stream_id: streamId, source: 'live' }, redacted));
549
+ }
441
550
  return;
442
551
  }
443
552
  // Python: the live chat frame carries the queue head plus depth, not the entry that just arrived.
@@ -447,6 +556,14 @@ export class TurnRunner {
447
556
  put('approval', { ...(pending ?? data), pending_count });
448
557
  return;
449
558
  }
559
+ // TAL-514: the Agent stopped waiting on an approval (answer, timeout, or interrupt); withdraw its copy.
560
+ case 'approval_resolved': {
561
+ if (!str(data.approval_id))
562
+ return;
563
+ deps.pending.resolveApproval(sessionId, str(data.approval_id));
564
+ put(...this.approvalHeadFrame(sessionId));
565
+ return;
566
+ }
450
567
  case 'clarify': {
451
568
  deps.pending.submitClarify(sessionId, { ...data, session_id: sessionId, kind: 'clarify' });
452
569
  const head = deps.pending.clarifyHeadFrame(sessionId);
@@ -470,33 +587,21 @@ export class TurnRunner {
470
587
  },
471
588
  });
472
589
  settledAt.value = true;
590
+ // TAL-545: every transcript the Agent hands back settles with the history's images, not the request's projection.
591
+ const withImages = (rows) => withNativeImagesRestored(rows, conversationHistory, apiHistory);
592
+ result.messages = withImages(result.messages);
593
+ if (result.context_messages)
594
+ result.context_messages = withImages(result.context_messages);
473
595
  if (this.registry.cancelled.has(streamId) || result.status === 'cancelled') {
474
596
  // The Stop's pre-interrupt snapshot is the boundary, even when its reply lands after this result (the sidecar
475
597
  // answers each request on its own thread); without one, the Agent's interrupted result is its canonical transcript.
476
598
  const interrupted = await this.interrupts.get(streamId);
477
- this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ?? result.messages);
599
+ applyAgentUsage(liveUsage, result.usage);
600
+ this.liveUsage.set(streamId, liveUsage);
601
+ this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ? withImages(interrupted.checkpoint) : result.messages);
478
602
  put('cancel', this.cancelFrame(sessionId));
479
603
  return;
480
604
  }
481
- if (opts.ephemeral) {
482
- let answer = '';
483
- for (let i = result.messages.length - 1; i >= 0; i -= 1) {
484
- const m = result.messages[i];
485
- if (m.role === 'assistant') {
486
- answer = str(m.content);
487
- break;
488
- }
489
- }
490
- opts.onDone?.(answer);
491
- // Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
492
- put('done', { session: { session_id: sessionId, messages: (result.messages).map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
493
- try {
494
- rmSync(deps.store.pathFor(sessionId), { force: true });
495
- }
496
- catch { /* ignore */ }
497
- deps.store.sessions.delete(sessionId);
498
- return;
499
- }
500
605
  // Writeback only while this stream still owns the session (Python `_stream_writeback_is_current`).
501
606
  let current;
502
607
  try {
@@ -512,18 +617,23 @@ export class TurnRunner {
512
617
  s = current;
513
618
  // Python `_maybe_inject_max_iteration_summary_fallback`: an exhausted tool budget leaves the closing
514
619
  // explanation in `final_response` only, so it becomes the turn's assistant answer before anything else reads it.
515
- const resultMessages = result.tool_limit_reached ? injectMaxIterationSummaryFallback(result.messages, result.final_response) : result.messages;
620
+ const withFallback = (rows) => (result.tool_limit_reached ? injectMaxIterationSummaryFallback(rows, result.final_response) : rows);
621
+ const withoutRequest = (rows) => (result.tool_limit_reached ? withoutMaxIterationSummaryRequest(rows, result.max_iterations_summary_request, msgText) : rows);
622
+ const agentRows = withFallback(result.messages);
623
+ const resultMessages = withoutRequest(agentRows);
516
624
  // Python `_assistant_reply_added_after_current_turn`: replayed history never counts as this turn's answer (the
517
625
  // sidecar reports `completed` whenever a failed run still carries messages).
518
626
  // Python's second chance: a turn that emitted no new row still counts when the merged transcript it produced
519
627
  // ends on a final answer (the current user row or trailing tool activity makes it "lacking").
520
628
  const mergedForCheck = () => mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] });
521
- const assistantAdded = assistantReplyAddedAfterCurrentTurn(resultMessages, previousContext, msgText) || !sessionLacksFinalAssistantAnswer(mergedForCheck());
629
+ // TAL-512: a btw answer must be a reply this turn added: everything before it is the parent's replayed history.
630
+ const assistantAdded = assistantReplyAddedAfterCurrentTurn(resultMessages, previousContext, msgText) || (!opts.ephemeral && !sessionLacksFinalAssistantAnswer(mergedForCheck()));
522
631
  const lastErr = result.error ?? capturedTerminalError ?? '';
523
- // Python `_turn_transcript_lacks_final_assistant_answer`: a partial result with no final answer is a silent failure even if tokens streamed.
524
- const stalePartial = result.result_status === 'partial' && !assistantAdded;
525
- if (result.status === 'error' || (!assistantAdded && !tokenSent) || stalePartial) {
526
- const classification = classifyProviderError(lastErr, { silentFailure: !lastErr });
632
+ // Python `_agent_result_terminal_failure`: the Agent's failed, partial, or compression-exhausted result ends the turn
633
+ // even after text streamed; only a partial with no error text and a final answer still completes.
634
+ const agentFailed = result.failed || result.compression_exhausted || (result.partial && (Boolean(lastErr) || !assistantAdded));
635
+ if (result.status === 'error' || agentFailed || (!assistantAdded && (!tokenSent || opts.ephemeral))) {
636
+ const classification = classifyProviderError(lastErr, { silentFailure: !lastErr, compressionExhausted: result.compression_exhausted });
527
637
  const errStr = lastErr || `${classification.label}.`;
528
638
  const payload = providerErrorPayload(errStr, classification.type, classification.hint, deps.redactEnabled());
529
639
  // Settle the steers first so the persisted turn carries every consumed one; the Agent's pending text is sent next.
@@ -531,8 +641,10 @@ export class TurnRunner {
531
641
  await this.steerRewrites.get(streamId);
532
642
  const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, str(result.pending_steer), 'followup');
533
643
  followUp = leftovers;
534
- this.persistError(s, streamId, classification.label, payload, activeTurnToken);
535
- payload.session = redactSessionData(this.terminalSessionPayload(s), deps.redactEnabled());
644
+ this.persistError(s, streamId, classification.label, payload, activeTurnToken, resultMessages);
645
+ // TAL-512: a btw error carries no session: its journaled frame must not keep a copy of the parent conversation.
646
+ if (!opts.ephemeral)
647
+ payload.session = redactSessionData(this.terminalSessionPayload(s), deps.redactEnabled());
536
648
  payload.session_id = s.session_id;
537
649
  payload.old_session_id = sessionId;
538
650
  for (const [event, data] of steerEvents)
@@ -541,21 +653,57 @@ export class TurnRunner {
541
653
  failed = true;
542
654
  return;
543
655
  }
656
+ // TAL-512: a btw turn passes the failure check above first, so its last assistant row is this turn's reply.
657
+ if (opts.ephemeral) {
658
+ let answer = '';
659
+ for (let i = resultMessages.length - 1; i >= 0; i -= 1) {
660
+ const m = resultMessages[i];
661
+ if (m.role === 'assistant') {
662
+ answer = str(m.content);
663
+ break;
664
+ }
665
+ }
666
+ opts.onDone?.(answer);
667
+ // Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
668
+ put('done', { session: { session_id: sessionId, messages: resultMessages.map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
669
+ try {
670
+ rmSync(deps.store.pathFor(sessionId), { force: true });
671
+ }
672
+ catch { /* ignore */ }
673
+ deps.store.sessions.delete(sessionId);
674
+ return;
675
+ }
544
676
  // ── settle the transcript ──
677
+ // TAL-539: after a mid-turn compression the model context is the sidecar's pruned copy, settled like the transcript.
678
+ const pruned = result.compressed ? result.context_messages ?? null : null;
679
+ const context = withoutToolImages(pruned ? withoutRequest(withFallback(pruned)) : resultMessages);
680
+ let estimate = result.compressed ? result.post_compression_context_tokens_estimate ?? null : null;
681
+ // The sidecar estimated its own rows; a tool-limit settlement or stripped screenshots (TAL-544) that changed them move
682
+ // the estimate by the difference.
683
+ if (pruned && estimate !== null && context !== pruned) {
684
+ try {
685
+ const [before, after] = await Promise.all([pruned, context].map((messages) => sidecar.call('models.estimate_tokens', { messages })));
686
+ estimate = Math.max(1, estimate + after.tokens - before.tokens);
687
+ }
688
+ catch {
689
+ estimate = null;
690
+ }
691
+ }
545
692
  // The Agent's last pending-steer text settles the remaining steers before the turn is written back.
546
693
  await this.steerRewrites.get(streamId);
547
694
  const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, result.pending_steer, 'followup');
548
- s.messages = mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] });
549
- s.context_messages = dedupeContext(resultMessages);
550
- for (const m of s.messages) {
551
- if (m.role !== 'assistant')
552
- continue;
553
- if (typeof m.content === 'string')
554
- m.content = stripXmlToolCalls(m.content);
555
- }
695
+ s.messages = withoutToolImages(mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] }));
696
+ s.context_messages = dedupeContext(context);
556
697
  if (result.compressed) {
557
- s.compression_anchor_visible_idx = Math.max(0, previousMessages.length - 1);
558
- put('compressed', { session_id: sessionId, old_session_id: sessionId, new_session_id: sessionId, continuation_session_id: sessionId, message: 'Compression finished' });
698
+ s.post_compression_context_tokens_estimate = estimate;
699
+ // TAL-540: the anchor is the last visible row before this turn (never its eager checkpoint); the merged transcript
700
+ // holds no compaction marker, so the summary is the model context's newest one.
701
+ const before = visibleMessagesForAnchor(previousMessages.filter((m) => m._turn_id !== streamId));
702
+ const after = visibleMessagesForAnchor(s.messages);
703
+ const anchorIdx = before.length ? before.length - 1 : after.length ? 0 : null;
704
+ s.compression_anchor_visible_idx = anchorIdx;
705
+ s.compression_anchor_message_key = anchorMessageKey(anchorIdx !== null && anchorIdx < after.length ? after[anchorIdx] : after.at(-1));
706
+ s.compression_anchor_summary = markerSummary(s.context_messages);
559
707
  }
560
708
  const now = deps.now();
561
709
  for (const m of s.messages)
@@ -565,22 +713,21 @@ export class TurnRunner {
565
713
  const usage = result.usage;
566
714
  const prevInputTokens = s.input_tokens || 0;
567
715
  const prevCacheReadTokens = s.cache_read_tokens || 0;
568
- if (usage.prompt_tokens > 0)
569
- s.input_tokens = usage.prompt_tokens;
570
- if (usage.completion_tokens > 0)
571
- s.output_tokens = usage.completion_tokens;
572
- if (usage.estimated_cost_usd !== null)
573
- s.estimated_cost = usage.estimated_cost_usd;
574
- if (usage.cache_read_tokens > 0)
575
- s.cache_read_tokens = usage.cache_read_tokens;
576
- if (usage.cache_write_tokens > 0)
577
- s.cache_write_tokens = usage.cache_write_tokens;
716
+ applyAgentUsage(s, usage);
578
717
  if (typeof result.context.context_length === 'number')
579
718
  s.context_length = result.context.context_length;
580
719
  if (typeof result.context.threshold_tokens === 'number')
581
720
  s.threshold_tokens = result.context.threshold_tokens;
582
721
  if (typeof result.context.last_prompt_tokens === 'number')
583
722
  s.last_prompt_tokens = result.context.last_prompt_tokens;
723
+ if (result.compressed) {
724
+ // TAL-540, Python `_live_usage_snapshot`: the counters and the ring's post-compression figures, so clients update before `done`.
725
+ const usage = {
726
+ ...meteringUsage(s), context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
727
+ post_compression_context_tokens_estimate: s.post_compression_context_tokens_estimate, ...contextUsage(s, () => deps.service().deps.contextLengthFor(s.model, s.model_provider)),
728
+ };
729
+ put('compressed', { session_id: sessionId, old_session_id: sessionId, new_session_id: sessionId, continuation_session_id: sessionId, message: 'Compression finished', usage });
730
+ }
584
731
  s.tool_calls = extractToolCallsFromMessages(s.messages, liveToolCalls, s.tool_calls);
585
732
  s.active_stream_id = null;
586
733
  s.pending_user_message = null;
@@ -611,12 +758,12 @@ export class TurnRunner {
611
758
  asstIdx += 1;
612
759
  if (turnIdx < prevAssistants)
613
760
  continue;
614
- const existing = str(m.reasoning);
761
+ // The settled row keeps its own fields: only its prose and its inline thinking (into `reasoning`) are rewritten.
615
762
  if (typeof m.content === 'string' && m.content) {
616
- const [content, merged] = splitThinkingFromContent(m.content, existing);
763
+ const [content, inline] = splitDisplayText(m.content);
617
764
  m.content = content;
618
- if (merged)
619
- m.reasoning = merged;
765
+ if (inline)
766
+ m.reasoning = joinReasoning([reasoningFieldsText({ reasoning: m.reasoning }), inline]);
620
767
  }
621
768
  if (turnIdx === prevAssistants && reasoning.trim() && !m.reasoning)
622
769
  m.reasoning = reasoning.trim();
@@ -637,6 +784,9 @@ export class TurnRunner {
637
784
  }
638
785
  }
639
786
  this.persistConsumedSteers(s, streamId, previousStartedAt(s, activeRun), now);
787
+ // TAL-493: the rows this turn's Agent wrote to state.db are now in the transcript, so the merge must not replay them
788
+ // (all of them, the dropped summary request too).
789
+ deps.service().settleStateDb(s, { turnId: streamId, previousContext: this.stopContexts.get(streamId)?.previousContext ?? previousContext, agentRows, startId: this.stopContexts.get(streamId)?.stateDbStartId ?? null });
640
790
  deps.store.save(s);
641
791
  deps.pending.clearApprovals(sessionId);
642
792
  deps.pending.clearClarifies(sessionId);
@@ -681,6 +831,8 @@ export class TurnRunner {
681
831
  };
682
832
  if (result.tool_limit_reached)
683
833
  donePayload.terminal_reason = 'max_iterations';
834
+ // TAL-397: the live meter ends on the persisted counters and turn rate.
835
+ put('metering', { session_id: sessionId, usage: meteringUsage(s), tps: doneUsage.tps ?? null, tps_available: doneUsage.tps !== undefined, estimated: false });
684
836
  put('done', donePayload);
685
837
  for (const [event, payload] of steerEvents)
686
838
  put(event, payload);
@@ -690,22 +842,16 @@ export class TurnRunner {
690
842
  this.registry.activeRuns.delete(streamId);
691
843
  this.settledStreams.add(streamId);
692
844
  this.startSteerFollowUp(sessionId, leftovers);
693
- await this.backgroundTitle(s, put);
845
+ // Before the title work, so a generated title's own state.db sync lands after this one's provisional title.
846
+ await deps.syncUsage?.(s, usedModel || null);
847
+ // Title work and the goal judge run side by side; `stream_end` follows both, so their frames reach the stream.
848
+ const work = await Promise.allSettled([this.backgroundTitle(s, put), this.continueGoal(s, streamId, put)]);
849
+ put('stream_end', { session_id: sessionId });
850
+ for (const r of work)
851
+ if (r.status === 'rejected')
852
+ throw r.reason;
694
853
  // Python: the last non-error assistant reply with content, else "(no answer produced)".
695
- if (opts.onDone) {
696
- let answer = '';
697
- for (let i = s.messages.length - 1; i >= 0; i -= 1) {
698
- const m = s.messages[i];
699
- if (m.role !== 'assistant' || m._error)
700
- continue;
701
- const text = messageText(m.content).trim();
702
- if (text) {
703
- answer = text;
704
- break;
705
- }
706
- }
707
- opts.onDone(answer || '(no answer produced)');
708
- }
854
+ opts.onDone?.(lastAnswer(s.messages) || '(no answer produced)');
709
855
  }
710
856
  catch (error) {
711
857
  if (settledAt.value && !(error instanceof SidecarError)) {
@@ -737,7 +883,8 @@ export class TurnRunner {
737
883
  for (const [event, data] of this.takeSteerEventsBefore(streamId, 'apperror'))
738
884
  put(event, data);
739
885
  this.persistError(current, streamId, classification.label, payload, activeTurnToken);
740
- payload.session = redactSessionData(this.terminalSessionPayload(current), deps.redactEnabled());
886
+ if (!opts.ephemeral)
887
+ payload.session = redactSessionData(this.terminalSessionPayload(current), deps.redactEnabled());
741
888
  }
742
889
  }
743
890
  catch (persistError) {
@@ -757,45 +904,49 @@ export class TurnRunner {
757
904
  }
758
905
  }
759
906
  /**
760
- * Python `_build_user_message`: image attachments are embedded as native `image_url` parts only when the Agent's
761
- * resolved image mode for this model is `native` (text mode routes them through the Agent's vision tool path), and
762
- * only after the bytes are read through an anchored descriptor and sniffed as a real image format. Every attached
763
- * file is also named by path after the text (TAL-276), so an attachment-only turn still gives the model a request
764
- * and its transcript row a distinct identity.
907
+ * Python `_resolve_image_input_mode`: the Agent's image mode for this turn's model under its profile config, or null
908
+ * when it is unknown (no sidecar, a cancel, a failed lookup). A canonical "text" is honoured only with an explicit
909
+ * text signal or a model KNOWN to be text-only; an unknown/custom model forwards natively and lets the Agent's retry
910
+ * guard downgrade.
765
911
  */
766
- async buildUserMessage(workspaceCtx, msgText, attachments, workspace, sessionId, s, opts, signal) {
767
- const text = workspaceCtx + msgText;
768
- const named = attachments.map((att) => str(att.path).trim()).filter(Boolean).map(escapeWorkspacePrefixPath);
769
- const withFiles = () => (named.length ? `${text}\n\n[Attached files: ${named.join(', ')}]` : text);
770
- const candidates = attachments.filter((att) => str(att.path).trim() && str(att.mime).trim().startsWith('image/'));
771
- if (!candidates.length)
772
- return withFiles();
912
+ async imageInputMode(sessionId, s, opts, signal) {
773
913
  const sidecar = this.deps.sidecar();
774
- if (!sidecar)
775
- return withFiles();
776
914
  // A cancel that landed before this point is final: never start the lookup or wait on it.
777
- if (signal.aborted)
778
- return text;
915
+ if (!sidecar || signal.aborted)
916
+ return null;
917
+ // The session's provider before the Agent canonicalizes it, so a `custom:<name>` override entry still matches.
918
+ const provider = str(opts.modelProvider ?? s.model_provider);
779
919
  try {
780
920
  // A cancelled turn must not sit behind this lookup: the abort wins the race and `run()` then takes the cancelled path.
781
- const lookup = sidecar.call('text.image_mode', { profile_home: this.deps.profileHome(s.profile), provider: str(opts.modelProvider ?? s.model_provider), model: str(opts.model ?? s.model) }, { signal, timeoutMs: IMAGE_MODE_TIMEOUT_MS });
921
+ const lookup = sidecar.call('text.image_mode', { profile_home: this.deps.profileHome(s.profile), provider, model: str(opts.model ?? s.model), requested_provider: provider }, { signal, timeoutMs: IMAGE_MODE_TIMEOUT_MS });
782
922
  const mode = await Promise.race([lookup, new Promise((_, reject) => { if (signal.aborted) {
783
923
  reject(new Error('turn cancelled'));
784
924
  return;
785
925
  } signal.addEventListener('abort', () => { reject(new Error('turn cancelled')); }, { once: true }); })]);
786
- // Python `_resolve_image_input_mode`: a canonical "text" is honoured only with an explicit text signal or a model
787
- // KNOWN to be text-only; an unknown/custom model forwards natively and lets the Agent's retry guard downgrade.
788
- if (mode.mode !== 'native') {
789
- const cfg = (await this.deps.profileConfig?.(s.profile ?? null)) ?? {};
790
- if (explicitTextSignal(cfg) || mode.supports_vision === false)
791
- return withFiles();
792
- }
926
+ if (mode.mode === 'native')
927
+ return 'native';
928
+ const cfg = (await this.deps.profileConfig?.(s.profile ?? null)) ?? {};
929
+ return explicitTextSignal(cfg) || mode.supports_vision === false ? 'text' : 'native';
793
930
  }
794
931
  catch (error) {
795
932
  if (!signal.aborted)
796
933
  this.deps.log(`[webui] image mode lookup failed for ${sessionId}: ${error.message}`);
797
- return withFiles();
934
+ return null;
798
935
  }
936
+ }
937
+ /**
938
+ * Python `_build_user_message`: image attachments are embedded as native `image_url` parts only when the turn's
939
+ * image mode is `native` (text mode routes them through the Agent's vision tool path), and only after the bytes are
940
+ * read through an anchored descriptor and sniffed as a real image format. Every attached file is also named by path
941
+ * after the text (TAL-276), so an attachment-only turn still gives the model a request and its transcript row a
942
+ * distinct identity.
943
+ */
944
+ buildUserMessage(workspaceCtx, msgText, attachments, workspace, sessionId, imageMode) {
945
+ const text = workspaceCtx + msgText;
946
+ const withFiles = () => attachedFilesPrompt(text, attachments);
947
+ const candidates = attachments.filter(isImageAttachment);
948
+ if (!candidates.length || imageMode !== 'native')
949
+ return withFiles();
799
950
  const parts = [];
800
951
  let images = 0;
801
952
  const roots = [workspace, this.deps.attachmentDir(sessionId)].map((r) => resolvePathLikePython(r));
@@ -835,21 +986,24 @@ export class TurnRunner {
835
986
  * msg_limit=` (renderable rows, limited payload shape, windowed tool calls, todo state) with the full count.
836
987
  */
837
988
  terminalSessionPayload(s) {
838
- const payload = withSessionWireFlags(s.compact({ contextLengthFor: this.deps.service().deps.contextLengthFor }), this.registry.liveIds);
989
+ const payload = withSessionWireFlags(s.compact({ contextLengthFor: this.deps.service().deps.contextLengthFor, modelOptionFor: this.deps.service().deps.modelOptionFor }), this.registry.liveIds);
839
990
  payload.assistant_name = this.deps.service().assistantName(s);
840
- const scened = withBodyExcerpts(this.deps.service().backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withTurnIds(withAttachmentObjects(s.messages)), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, clipToolResults: true })), s.active_stream_id);
991
+ payload.workspace_name = this.deps.service().workspaceNames()(s);
992
+ const service = this.deps.service();
993
+ const scened = service.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withMarkerKinds(withTurnIds(withAttachmentObjects(s.messages))), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, clipToolResults: true }));
841
994
  const [window, offset] = messageWindowForDisplay(scened, TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT, null);
842
- const limited = messagesForLimitedPayload(window);
995
+ const limited = messagesForLimitedPayload(withBodyExcerpts(withDisplayMedia(window, service.mediaProjector(s)), s.active_stream_id));
843
996
  payload.messages = limited;
844
997
  payload.message_count = s.messages.length;
845
998
  payload._messages_offset = offset;
846
999
  payload._messages_truncated = offset > 0;
847
- attachTodoState(payload, s.messages);
1000
+ attachTodoState(payload, s.messages, s.extra[UNSETTLED_TODO_KEY]);
848
1001
  payload.tool_calls = toolCallsForMessageWindow(s.tool_calls, offset, limited.length);
1002
+ payload.compression_reference = compressionReference(s, scened);
849
1003
  return payload;
850
1004
  }
851
1005
  /** Python `_materialize_pending_user_turn_before_error` + error message append + save. */
852
- persistError(s, streamId, label, payload, activeTurnToken) {
1006
+ persistError(s, streamId, label, payload, activeTurnToken, agentRows = []) {
853
1007
  const startedAt = s.pending_started_at;
854
1008
  this.materializePendingUserTurn(s, activeTurnToken, streamId);
855
1009
  const duration = typeof startedAt === 'number' && startedAt > 0 ? Math.max(0, this.deps.now() - startedAt) : null;
@@ -879,13 +1033,18 @@ export class TurnRunner {
879
1033
  else if (payload.type === 'interrupted')
880
1034
  errorMessage.provider_details_label = 'Interruption details';
881
1035
  s.messages.push(errorMessage);
1036
+ this.keepLiveState(s, streamId);
882
1037
  this.persistConsumedSteers(s, streamId, startedAt, this.deps.now());
1038
+ const stop = this.stopContexts.get(streamId);
1039
+ this.deps.service().settleStateDb(s, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows, startId: stop?.stateDbStartId ?? null });
883
1040
  try {
884
1041
  this.deps.store.save(s);
885
1042
  }
886
1043
  catch (error) {
887
1044
  this.deps.log(`[webui] WARNING: failed to save error turn for ${s.session_id}: ${error.message}`);
888
1045
  }
1046
+ // The kept live counters are the session's usage now, so the insights row follows them (the runtime logs failures).
1047
+ void this.deps.syncUsage?.(s, str(s.model) || null);
889
1048
  this.deps.pending.clearApprovals(s.session_id);
890
1049
  this.deps.pending.clearClarifies(s.session_id);
891
1050
  this.deps.events.publish('session_error', { profile: s.profile, sessionId: s.session_id });
@@ -974,8 +1133,19 @@ export class TurnRunner {
974
1133
  // TAL-460: a background turn stops quietly: its prompt and model context are kept, but no partial and no Stop row,
975
1134
  // and its first settlement is final.
976
1135
  const quiet = this.registry.activeRuns.get(streamId)?.origin === 'background';
977
- if (quiet && current.active_stream_id === null)
1136
+ if (quiet && current.active_stream_id === null) {
1137
+ // TAL-493: rows its worker committed while unwinding are covered by what it reports; others are kept.
1138
+ if (checkpoint) {
1139
+ this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: this.stopContexts.get(streamId)?.previousContext ?? [], agentRows: checkpoint, startId: this.stopContexts.get(streamId)?.stateDbStartId ?? null });
1140
+ try {
1141
+ this.deps.store.save(current);
1142
+ }
1143
+ catch {
1144
+ return false;
1145
+ }
1146
+ }
978
1147
  return true;
1148
+ }
979
1149
  const stop = this.stopContexts.get(streamId);
980
1150
  const canonical = stop !== undefined && checkpoint !== null && checkpointTurnStart(checkpoint, stop.msgText, stop.historyLength) !== null;
981
1151
  const settle = () => {
@@ -989,9 +1159,13 @@ export class TurnRunner {
989
1159
  // The worker's canonical result can arrive after cancel() settled without a checkpoint (the interrupt reply failed
990
1160
  // or timed out): it replaces this stream's context and never adds a second Stop row. A settlement that already had
991
1161
  // one keeps it, so work the Agent finished after the Stop stays out.
992
- const late = canonical && !stop.checkpointed && current.messages.some((m) => isCancelMarker(m) && m._turn_id === streamId) ? settle() : null;
993
- if (late) {
1162
+ const ownStop = current.messages.some((m) => isCancelMarker(m) && m._turn_id === streamId);
1163
+ const late = canonical && !stop.checkpointed && ownStop ? settle() : null;
1164
+ if (late)
994
1165
  current.context_messages = dedupeContext(late);
1166
+ // TAL-493: the worker's rows committed after the first settlement are covered by what it reports; others are kept.
1167
+ if (late || (ownStop && checkpoint)) {
1168
+ this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows: checkpoint, startId: stop?.stateDbStartId ?? null });
995
1169
  try {
996
1170
  this.deps.store.save(current);
997
1171
  }
@@ -1015,13 +1189,16 @@ export class TurnRunner {
1015
1189
  this.appendPartialSnapshot(current, streamId);
1016
1190
  current.messages.push({ role: 'assistant', content: '', _error: true, _terminal_state: 'cancelled', timestamp: Math.trunc(this.deps.now()), _turn_id: streamId });
1017
1191
  }
1192
+ this.keepLiveState(current, streamId);
1018
1193
  this.persistConsumedSteers(current, streamId, startedAt, this.deps.now());
1194
+ this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows: checkpoint, startId: stop?.stateDbStartId ?? null });
1019
1195
  try {
1020
1196
  this.deps.store.save(current);
1021
1197
  }
1022
1198
  catch {
1023
1199
  return false;
1024
1200
  }
1201
+ void this.deps.syncUsage?.(current, str(current.model) || null);
1025
1202
  this.deps.pending.clearApprovals(current.session_id);
1026
1203
  this.deps.pending.clearClarifies(current.session_id);
1027
1204
  this.deps.events.publish('session_cancel', { profile: current.profile, sessionId: current.session_id });
@@ -1031,7 +1208,18 @@ export class TurnRunner {
1031
1208
  emitToSession(sessionId, event, data) {
1032
1209
  this.sessionPuts.get(sessionId)?.(event, data);
1033
1210
  }
1211
+ /** TAL-397: a turn that ends without its result keeps what the live stream already showed: counters and todo list. */
1212
+ keepLiveState(s, streamId) {
1213
+ const usage = this.liveUsage.get(streamId);
1214
+ if (usage)
1215
+ Object.assign(s, usage);
1216
+ const todos = this.liveTodos.get(streamId);
1217
+ if (todos)
1218
+ s.extra[UNSETTLED_TODO_KEY] = todos;
1219
+ }
1034
1220
  teardown(sessionId, streamId) {
1221
+ this.liveUsage.delete(streamId);
1222
+ this.liveTodos.delete(streamId);
1035
1223
  const writer = this.writers.get(streamId);
1036
1224
  if (writer) {
1037
1225
  try {
@@ -1099,13 +1287,14 @@ export class TurnRunner {
1099
1287
  }
1100
1288
  return { title: '', status, rawPreview: '' };
1101
1289
  }
1102
- /** Python `generate_session_title_for_session`: on-demand title from the persisted transcript; never touches `llm_title_generated`. */
1290
+ /**
1291
+ * Python `generate_session_title_for_session`: on-demand title from the persisted transcript; never touches
1292
+ * `llm_title_generated`. An explicit regenerate runs even with `auxiliary.title_generation.enabled` off (TAL-531).
1293
+ */
1103
1294
  async generateTitle(s, opts = {}) {
1104
1295
  const [userText, assistantText] = opts.preferLatest ? latestExchangeSnippets(s.messages) : firstExchangeSnippets(s.messages, { scanPastConsecutiveUsers: true });
1105
1296
  if (!userText)
1106
1297
  return { title: null, status: 'empty_user_message', rawPreview: '' };
1107
- if (!this.deps.titleGenerationEnabled())
1108
- return { title: null, status: 'title_generation_disabled', rawPreview: '' };
1109
1298
  let next = '';
1110
1299
  let llmStatus = 'empty_assistant_message';
1111
1300
  let rawPreview = '';
@@ -1131,65 +1320,66 @@ export class TurnRunner {
1131
1320
  const status = (status, reason = '', title = '', rawPreview = '') => {
1132
1321
  put('title_status', { session_id: sessionId, status, ...(reason ? { reason } : {}), ...(title ? { title } : {}), ...(rawPreview ? { raw_preview: rawPreview } : {}) });
1133
1322
  };
1134
- try {
1135
- if (!eligible || (s.llm_title_generated && !invalidExisting) || !userText || !assistantText)
1136
- return;
1137
- if (s.manual_title) {
1138
- status('skipped', 'manual_title', placeholder);
1139
- return;
1323
+ if (!eligible || (s.llm_title_generated && !invalidExisting) || !userText || !assistantText)
1324
+ return;
1325
+ if (s.manual_title) {
1326
+ status('skipped', 'manual_title', placeholder);
1327
+ return;
1328
+ }
1329
+ // A missing config.yaml reads as `{}` (enabled); an unreadable one cannot confirm the user has not opted out.
1330
+ const cfg = await this.deps.profileConfig?.(s.profile ?? null);
1331
+ if (!cfg) {
1332
+ status('skipped', 'config_unavailable', placeholder);
1333
+ return;
1334
+ }
1335
+ if (!titleGenerationEnabled(cfg)) {
1336
+ status('skipped', 'title_generation_disabled', placeholder);
1337
+ return;
1338
+ }
1339
+ const generated = await this.llmTitle(s, userText, assistantText);
1340
+ const { status: llmStatus, rawPreview } = generated;
1341
+ let next = generated.title;
1342
+ let source = llmStatus;
1343
+ if (!next) {
1344
+ const fallback = fallbackTitleFromExchange(userText, assistantText);
1345
+ if (fallback && !isGenericFallbackTitle(fallback)) {
1346
+ next = fallback;
1347
+ source = 'fallback';
1140
1348
  }
1141
- if (!this.deps.titleGenerationEnabled()) {
1142
- status('skipped', 'title_generation_disabled', placeholder);
1349
+ }
1350
+ const fallbackReason = source === 'fallback' && llmStatus ? `local_summary:${llmStatus}` : 'local_summary';
1351
+ let current = s;
1352
+ try {
1353
+ current = this.deps.store.get(sessionId);
1354
+ }
1355
+ catch {
1356
+ current = s;
1357
+ }
1358
+ let effective = str(current.title).trim();
1359
+ let wrote = false;
1360
+ if (next) {
1361
+ const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title) || defaultTitle(current);
1362
+ if (current.manual_title || !stillAuto) {
1363
+ status('skipped', 'manual_title', effective);
1143
1364
  return;
1144
1365
  }
1145
- const generated = await this.llmTitle(s, userText, assistantText);
1146
- const { status: llmStatus, rawPreview } = generated;
1147
- let next = generated.title;
1148
- let source = llmStatus;
1149
- if (!next) {
1150
- const fallback = fallbackTitleFromExchange(userText, assistantText);
1151
- if (fallback && !isGenericFallbackTitle(fallback)) {
1152
- next = fallback;
1153
- source = 'fallback';
1154
- }
1366
+ if (next !== effective) {
1367
+ current.title = next;
1368
+ markSessionTitleGenerated(current);
1369
+ this.deps.store.save(current, { touchUpdatedAt: false });
1370
+ // Python `sync_session_title` after generation: the state.db row follows when `sync_to_insights` is on.
1371
+ await this.deps.syncTitle?.(current);
1372
+ this.deps.events.publish('title', { profile: current.profile, sessionId });
1373
+ effective = next;
1374
+ wrote = true;
1155
1375
  }
1156
- const fallbackReason = source === 'fallback' && llmStatus ? `local_summary:${llmStatus}` : 'local_summary';
1157
- let current = s;
1158
- try {
1159
- current = this.deps.store.get(sessionId);
1160
- }
1161
- catch {
1162
- current = s;
1163
- }
1164
- let effective = str(current.title).trim();
1165
- let wrote = false;
1166
- if (next) {
1167
- const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title) || defaultTitle(current);
1168
- if (current.manual_title || !stillAuto) {
1169
- status('skipped', 'manual_title', effective);
1170
- return;
1171
- }
1172
- if (next !== effective) {
1173
- current.title = next;
1174
- markSessionTitleGenerated(current);
1175
- this.deps.store.save(current, { touchUpdatedAt: false });
1176
- // Python `sync_session_title` after generation: the state.db row follows when `sync_to_insights` is on.
1177
- await this.deps.syncTitle?.(current);
1178
- this.deps.events.publish('title', { profile: current.profile, sessionId });
1179
- effective = next;
1180
- wrote = true;
1181
- }
1182
- }
1183
- if (wrote) {
1184
- status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
1185
- put('title', { session_id: sessionId, title: effective });
1186
- }
1187
- else
1188
- status('skipped', source || 'unchanged', effective, rawPreview);
1189
1376
  }
1190
- finally {
1191
- put('stream_end', { session_id: sessionId });
1377
+ if (wrote) {
1378
+ status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
1379
+ put('title', { session_id: sessionId, title: effective });
1192
1380
  }
1381
+ else
1382
+ status('skipped', source || 'unchanged', effective, rawPreview);
1193
1383
  }
1194
1384
  // ── cancel / steer ───────────────────────────────────────────────────────
1195
1385
  /**
@@ -1503,6 +1693,60 @@ export class TurnRunner {
1503
1693
  }
1504
1694
  this.deps.log(`[webui] WARNING: steer follow-up for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
1505
1695
  }
1696
+ /**
1697
+ * TAL-396 (Agent gateway `_post_turn_goal_continuation`): after every turn settles, the Agent's GoalManager judges it
1698
+ * while a goal is active and the server starts the continuation turn it asks for. Any turn counts, so a resumed or
1699
+ * persisted goal picks up from the next user message. A user turn that took the session meanwhile runs first and is
1700
+ * judged next. A stopped or failed turn never gets here, so the run ends there.
1701
+ */
1702
+ async continueGoal(s, streamId, put) {
1703
+ const sidecar = this.deps.sidecar();
1704
+ const lastResponse = lastAnswer(s.messages);
1705
+ // The Agent never drives a goal from an empty reply.
1706
+ if (!sidecar || !lastResponse)
1707
+ return;
1708
+ const sessionId = s.session_id;
1709
+ // Only a known goal turn shows progress up front; any other turn shows nothing unless a goal turns out to be active.
1710
+ const goalTurn = this.registry.goalRelated.has(streamId);
1711
+ if (goalTurn)
1712
+ put('goal', { session_id: sessionId, state: 'evaluating', message: 'Evaluating goal progress…', message_key: 'goal_evaluating_progress' });
1713
+ let decision;
1714
+ try {
1715
+ decision = await sidecar.call('goals.evaluate', { session_id: sessionId, profile_home: this.deps.profileHome(s.profile), last_response: lastResponse, user_initiated: true });
1716
+ }
1717
+ catch (error) {
1718
+ this.deps.log(`[webui] WARNING: goal evaluation failed for ${sessionId}: ${error.message}`);
1719
+ if (goalTurn)
1720
+ put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
1721
+ return;
1722
+ }
1723
+ if (!goalTurn && decision.verdict === 'inactive')
1724
+ return;
1725
+ const prompt = str(decision.continuation_prompt).trim();
1726
+ const proceed = decision.should_continue && Boolean(prompt);
1727
+ const status = { message: decision.message, message_key: decision.message_key, message_args: decision.message_args ?? [], decision: decision.verdict };
1728
+ put('goal', { session_id: sessionId, state: proceed ? 'continuing' : 'idle', ...status });
1729
+ if (!proceed)
1730
+ return;
1731
+ let current;
1732
+ try {
1733
+ current = this.deps.store.get(sessionId);
1734
+ }
1735
+ catch {
1736
+ return;
1737
+ }
1738
+ const started = this.start(current, { msg: prompt, attachments: [], workspace: current.workspace, model: current.model, modelProvider: current.model_provider, goalRelated: true });
1739
+ if (started._status === 409 && started.active_stream_id) {
1740
+ this.registry.goalRelated.add(started.active_stream_id);
1741
+ return;
1742
+ }
1743
+ if (started._status !== undefined && started._status >= 400) {
1744
+ this.deps.log(`[webui] WARNING: goal continuation for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
1745
+ put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
1746
+ return;
1747
+ }
1748
+ put('goal_continue', { session_id: sessionId, state: 'continuing', continuation_prompt: prompt, text: prompt, stream_id: str(started.stream_id), ...status });
1749
+ }
1506
1750
  /** The Agent's pending steer text settles which steers it has taken: those become `steer_consumed` and their rows. */
1507
1751
  settleConsumedSteers(sessionId, streamId, pendingText) {
1508
1752
  for (const record of this.takeConsumedSteers(streamId, pendingText, { keepLeftovers: true }))
@@ -1645,6 +1889,7 @@ export class TurnRunner {
1645
1889
  }
1646
1890
  const profileHome = this.deps.profileHome(s?.profile ?? null);
1647
1891
  let resolved = false;
1892
+ let withdrawn = false;
1648
1893
  if (entry) {
1649
1894
  if (!sidecar)
1650
1895
  return relayFailure('not running');
@@ -1656,8 +1901,13 @@ export class TurnRunner {
1656
1901
  this.deps.log(`[webui] approval relay failed for ${sessionId}: ${error.message}`);
1657
1902
  return relayFailure(str(error.message));
1658
1903
  }
1659
- if (resolved && entry)
1904
+ if (resolved)
1660
1905
  this.deps.pending.resolveApproval(sessionId, str(entry.approval_id));
1906
+ else {
1907
+ // TAL-514: a rejected answer can mean the Agent already stopped waiting on this card; drop what it no longer holds.
1908
+ await this.reconcileApprovals(sessionId, sidecar);
1909
+ withdrawn = !this.deps.pending.hasApprovalId(sessionId, str(entry.approval_id));
1910
+ }
1661
1911
  }
1662
1912
  let yoloEnabled;
1663
1913
  if (enableYolo) {
@@ -1680,10 +1930,34 @@ export class TurnRunner {
1680
1930
  yoloEnabled = true;
1681
1931
  this.deps.pending.clearApprovals(sessionId);
1682
1932
  }
1933
+ if (withdrawn)
1934
+ return { ok: true, choice, stale_cleared: true, pending_count: this.deps.pending.approvalPending(sessionId).pending_count, ...(enableYolo ? { yolo_enabled: yoloEnabled } : {}) };
1683
1935
  if (!found && !this.deps.pending.hasPendingApproval(sessionId))
1684
1936
  return { ok: true, choice, stale_cleared: true, ...(enableYolo ? { yolo_enabled: yoloEnabled } : {}) };
1685
1937
  return { ok: resolved || !approvalId, choice, ...(enableYolo && (resolved || !approvalId) ? { yolo_enabled: yoloEnabled } : {}) };
1686
1938
  }
1939
+ /** The live chat frame after approvals were withdrawn: the new queue head, or `approval_cleared` once none is left. */
1940
+ approvalHeadFrame(sessionId) {
1941
+ const { pending, pending_count } = this.deps.pending.approvalPending(sessionId);
1942
+ return pending ? ['approval', { ...pending, pending_count }] : ['approval_cleared', { session_id: sessionId, pending_count: 0 }];
1943
+ }
1944
+ /**
1945
+ * Drop mirrored approvals the Agent no longer holds; a failed read keeps them (the card stays answerable). Only
1946
+ * entries queued before the read are candidates: one the Agent parks meanwhile can be missing from its snapshot.
1947
+ */
1948
+ async reconcileApprovals(sessionId, sidecar) {
1949
+ const before = this.deps.pending.approvalRequestIds(sessionId);
1950
+ let live;
1951
+ try {
1952
+ live = new Set((await sidecar.call('approval.pending', { session_id: sessionId })).pending.map((p) => str(p.request_id)));
1953
+ }
1954
+ catch (error) {
1955
+ this.deps.log(`[webui] approval reconcile failed for ${sessionId}: ${error.message}`);
1956
+ return;
1957
+ }
1958
+ if (this.deps.pending.dropApprovals(sessionId, new Set(before.filter((id) => !live.has(id)))).length)
1959
+ this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId));
1960
+ }
1687
1961
  withYoloLock(sessionId, fn) {
1688
1962
  const run = (this.yoloChains.get(sessionId) ?? Promise.resolve()).then(fn);
1689
1963
  const settled = run.catch(() => undefined).then(() => { if (this.yoloChains.get(sessionId) === settled)
@@ -1736,6 +2010,18 @@ export class TurnRunner {
1736
2010
  return { ok: true, response };
1737
2011
  }
1738
2012
  }
2013
+ /** The turn's answer: the last non-error assistant reply with content. */
2014
+ function lastAnswer(messages) {
2015
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
2016
+ const m = messages[i];
2017
+ if (m.role !== 'assistant' || m._error)
2018
+ continue;
2019
+ const text = messageText(m.content).trim();
2020
+ if (text)
2021
+ return text;
2022
+ }
2023
+ return '';
2024
+ }
1739
2025
  function previousStartedAt(s, run) {
1740
2026
  return typeof s.pending_started_at === 'number' && s.pending_started_at > 0 ? s.pending_started_at : (run?.started_at ?? Date.now() / 1000);
1741
2027
  }
@@ -1743,18 +2029,6 @@ function previousStartedAt(s, run) {
1743
2029
  function isCancelMarker(m) {
1744
2030
  return m._error === true && (m._terminal_state === 'cancelled' || str(m.content).startsWith('**Task cancelled:**'));
1745
2031
  }
1746
- function dedupeContext(messages) {
1747
- const out = [];
1748
- let lastKey = null;
1749
- for (const m of messages) {
1750
- const key = messageIdentity(m);
1751
- if (key !== null && key === lastKey && m.role === 'assistant')
1752
- continue;
1753
- out.push(m);
1754
- lastKey = key;
1755
- }
1756
- return out;
1757
- }
1758
2032
  export { HttpFailure };
1759
2033
  /** Where a consumed steer goes: past the tool results of the call it followed, else past the turn's prompt. */
1760
2034
  function roundDuration(seconds) {