@maudecode/talaria-web 1.14.0 → 1.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/_release.json +4 -4
  2. package/dist/api/automation-router.d.ts +102 -0
  3. package/dist/api/automation-router.js +2 -0
  4. package/dist/api/automation-router.js.map +1 -1
  5. package/dist/api/chat-router.d.ts +589 -52
  6. package/dist/api/chat-router.js +111 -121
  7. package/dist/api/chat-router.js.map +1 -1
  8. package/dist/api/router.d.ts +2 -1
  9. package/dist/api/router.js +3 -1
  10. package/dist/api/router.js.map +1 -1
  11. package/dist/api/sessions-router.d.ts +1913 -299
  12. package/dist/api/sessions-router.js +35 -24
  13. package/dist/api/sessions-router.js.map +1 -1
  14. package/dist/api/settings-router.d.ts +35 -8
  15. package/dist/api/settings-router.js +14 -10
  16. package/dist/api/settings-router.js.map +1 -1
  17. package/dist/api/sse-routes.js +1 -1
  18. package/dist/api/sse-routes.js.map +1 -1
  19. package/dist/app.d.ts +2715 -418
  20. package/dist/config/agent-config.js +20 -15
  21. package/dist/config/agent-config.js.map +1 -1
  22. package/dist/http/context.d.ts +4 -2
  23. package/dist/http/context.js.map +1 -1
  24. package/dist/onboarding.d.ts +1 -1
  25. package/dist/onboarding.js +2 -1
  26. package/dist/onboarding.js.map +1 -1
  27. package/dist/providers/catalog.d.ts +11 -0
  28. package/dist/providers/catalog.js +32 -1
  29. package/dist/providers/catalog.js.map +1 -1
  30. package/dist/runtime.js +54 -16
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/sessions/background-tasks.d.ts +137 -0
  33. package/dist/sessions/background-tasks.js +349 -0
  34. package/dist/sessions/background-tasks.js.map +1 -0
  35. package/dist/sessions/background-updates.d.ts +56 -0
  36. package/dist/sessions/background-updates.js +152 -0
  37. package/dist/sessions/background-updates.js.map +1 -0
  38. package/dist/sessions/cli-sessions.d.ts +6 -0
  39. package/dist/sessions/cli-sessions.js +20 -7
  40. package/dist/sessions/cli-sessions.js.map +1 -1
  41. package/dist/sessions/completions.d.ts +17 -1
  42. package/dist/sessions/completions.js +83 -12
  43. package/dist/sessions/completions.js.map +1 -1
  44. package/dist/sessions/compress.d.ts +33 -0
  45. package/dist/sessions/compress.js +66 -0
  46. package/dist/sessions/compress.js.map +1 -0
  47. package/dist/sessions/list.d.ts +14 -0
  48. package/dist/sessions/list.js +89 -16
  49. package/dist/sessions/list.js.map +1 -1
  50. package/dist/sessions/merge.d.ts +12 -1
  51. package/dist/sessions/merge.js +64 -7
  52. package/dist/sessions/merge.js.map +1 -1
  53. package/dist/sessions/relay.d.ts +7 -1
  54. package/dist/sessions/relay.js +17 -4
  55. package/dist/sessions/relay.js.map +1 -1
  56. package/dist/sessions/service.d.ts +61 -3
  57. package/dist/sessions/service.js +243 -15
  58. package/dist/sessions/service.js.map +1 -1
  59. package/dist/sessions/session.d.ts +16 -0
  60. package/dist/sessions/session.js +23 -2
  61. package/dist/sessions/session.js.map +1 -1
  62. package/dist/sessions/source-kind.d.ts +22 -0
  63. package/dist/sessions/source-kind.js +54 -0
  64. package/dist/sessions/source-kind.js.map +1 -0
  65. package/dist/sessions/state-db.d.ts +10 -1
  66. package/dist/sessions/state-db.js +48 -17
  67. package/dist/sessions/state-db.js.map +1 -1
  68. package/dist/sessions/store.d.ts +2 -0
  69. package/dist/sessions/store.js +23 -4
  70. package/dist/sessions/store.js.map +1 -1
  71. package/dist/sessions/streams.d.ts +3 -0
  72. package/dist/sessions/streams.js +0 -5
  73. package/dist/sessions/streams.js.map +1 -1
  74. package/dist/sessions/titles.d.ts +4 -0
  75. package/dist/sessions/titles.js +16 -0
  76. package/dist/sessions/titles.js.map +1 -1
  77. package/dist/sessions/turn-context.d.ts +1 -0
  78. package/dist/sessions/turn-context.js +3 -0
  79. package/dist/sessions/turn-context.js.map +1 -1
  80. package/dist/sessions/turn.d.ts +66 -6
  81. package/dist/sessions/turn.js +304 -61
  82. package/dist/sessions/turn.js.map +1 -1
  83. package/dist/settings.d.ts +2 -0
  84. package/dist/settings.js +4 -0
  85. package/dist/settings.js.map +1 -1
  86. package/dist/tools/crons.d.ts +34 -3
  87. package/dist/tools/crons.js +271 -31
  88. package/dist/tools/crons.js.map +1 -1
  89. package/package.json +2 -2
  90. package/sidecar/talaria_sidecar/__init__.py +1 -1
  91. package/sidecar/talaria_sidecar/methods/chat.py +233 -6
  92. package/sidecar/talaria_sidecar/methods/cron.py +22 -14
  93. package/sidecar/talaria_sidecar/methods/process.py +318 -0
  94. package/static/dist/FILES.txt +120 -119
  95. package/static/dist/assets/AppShell-orat1s43.js +1 -0
  96. package/static/dist/assets/ChatPage-ojhka3hy.js +22 -0
  97. package/static/dist/assets/HubRoute-gxgw1t33.js +2 -0
  98. package/static/dist/assets/{Markdown-emii55o0.js → Markdown-dy8t4dy7.js} +7 -7
  99. package/static/dist/assets/{RightPanel-o73y98dn.js → RightPanel-c66ani1s.js} +1 -1
  100. package/static/dist/assets/Select-fswf66up.js +1 -0
  101. package/static/dist/assets/SessionListPanel-goh5mtt8.js +1 -0
  102. package/static/dist/assets/{SettingsLayout-lnn2wo2s.js → SettingsLayout-n5d4wq4m.js} +1 -1
  103. package/static/dist/assets/{States-e5hv6gdh.js → States-kxyu50at.js} +1 -1
  104. package/static/dist/assets/{Toaster-c4jq1i26.js → Toaster-6zmbcqgg.js} +1 -1
  105. package/static/dist/assets/_app-fowyruiu.js +1 -0
  106. package/static/dist/assets/_app.ext._extensionId-nvjyrwhy.js +1 -0
  107. package/static/dist/assets/_app.index-c9zv4y7w.js +1 -0
  108. package/static/dist/assets/_app.insights-jf12nofo.js +1 -0
  109. package/static/dist/assets/_app.kanban-lri5a4yu.js +1 -0
  110. package/static/dist/assets/_app.logs-md1p0zqo.js +1 -0
  111. package/static/dist/assets/_app.memory-bzvf2e28.js +1 -0
  112. package/static/dist/assets/_app.profiles-jpg7zmbn.js +1 -0
  113. package/static/dist/assets/{_app.session._sessionId-m6nx9g8t.js → _app.session._sessionId-gyoh5ylx.js} +1 -1
  114. package/static/dist/assets/_app.settings-mpuwlx4j.js +1 -0
  115. package/static/dist/assets/_app.settings._section-knz8rwoj.js +1 -0
  116. package/static/dist/assets/_app.skills-fwu1dkir.js +1 -0
  117. package/static/dist/assets/_app.tasks-h9gphtl3.js +2 -0
  118. package/static/dist/assets/_app.todos-hd1dnvrh.js +1 -0
  119. package/static/dist/assets/_app.workspaces-l8toyn5f.js +1 -0
  120. package/static/dist/assets/{abnfDiagram-VCTEODGH-kejgxpil.js → abnfDiagram-VCTEODGH-cjqletra.js} +1 -1
  121. package/static/dist/assets/architecture-7GRP2DOG-cavnusaw.js +1 -0
  122. package/static/dist/assets/{architectureDiagram-5GKGNRK7-hqnf84fc.js → architectureDiagram-5GKGNRK7-k5a7v36n.js} +1 -1
  123. package/static/dist/assets/back-e4m6nsnm.js +1 -0
  124. package/static/dist/assets/{blockDiagram-I7D4REHJ-l1466a7j.js → blockDiagram-I7D4REHJ-b195z021.js} +1 -1
  125. package/static/dist/assets/{c4Diagram-7LVT6UL2-gg3aq3sr.js → c4Diagram-7LVT6UL2-olu212k1.js} +1 -1
  126. package/static/dist/assets/channel-gozy3dp3.js +1 -0
  127. package/static/dist/assets/{chunk-4HAMMTFA-oalgz5l8.js → chunk-4HAMMTFA-b8o1nufs.js} +1 -1
  128. package/static/dist/assets/{chunk-75Z2AOVW-babmzhdg.js → chunk-75Z2AOVW-l1seddto.js} +1 -1
  129. package/static/dist/assets/{chunk-DU6HZSFF-lagu350c.js → chunk-DU6HZSFF-lo5zg15v.js} +3 -3
  130. package/static/dist/assets/{chunk-F27PBJKO-gjh2tqif.js → chunk-F27PBJKO-dsdzrp16.js} +1 -1
  131. package/static/dist/assets/{chunk-GMAD6QVW-ipdgja5c.js → chunk-GMAD6QVW-f3fxqq8b.js} +1 -1
  132. package/static/dist/assets/{chunk-GVQU2GXP-j8wkn1at.js → chunk-GVQU2GXP-h8egxtf6.js} +1 -1
  133. package/static/dist/assets/{chunk-IMKFNOWR-k00laqyk.js → chunk-IMKFNOWR-iolwd8s0.js} +1 -1
  134. package/static/dist/assets/{chunk-L3NEJ4N5-fju6xqj4.js → chunk-L3NEJ4N5-b88bbtwi.js} +1 -1
  135. package/static/dist/assets/{chunk-OSK3NFVY-bi6widqt.js → chunk-OSK3NFVY-e3e7nr6s.js} +1 -1
  136. package/static/dist/assets/{chunk-P2QGCYS3-czr4tebv.js → chunk-P2QGCYS3-mdc46rt1.js} +1 -1
  137. package/static/dist/assets/{chunk-POPQ4Y6H-ktdhn67w.js → chunk-POPQ4Y6H-kqlllrrm.js} +1 -1
  138. package/static/dist/assets/{chunk-PWAF6VOD-ii4dsu0d.js → chunk-PWAF6VOD-he585k2x.js} +1 -1
  139. package/static/dist/assets/{chunk-SHT3W25Y-fpwk3xxp.js → chunk-SHT3W25Y-gh94y9a4.js} +1 -1
  140. package/static/dist/assets/{chunk-SVP7TREG-k0jrvhei.js → chunk-SVP7TREG-euxp9y7e.js} +1 -1
  141. package/static/dist/assets/{chunk-TICWLB2K-nkqv9ot0.js → chunk-TICWLB2K-c8cxes1q.js} +1 -1
  142. package/static/dist/assets/classDiagram-ZZMXUADV-n0vs2f2q.js +1 -0
  143. package/static/dist/assets/classDiagram-v2-VYDZK3BY-n0vs2f2q.js +1 -0
  144. package/static/dist/assets/{copied-gpchexqu.js → copied-kq0u3ht7.js} +1 -1
  145. package/static/dist/assets/{cynefin-OW5HDTMX-lqxt8rs3.js → cynefin-OW5HDTMX-lxd44ux4.js} +1 -1
  146. package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-j9eqovf5.js → cynefinDiagram-5FMLGOSQ-njpiwat8.js} +1 -1
  147. package/static/dist/assets/{dagre-GXQ25YYZ-m9agobjm.js → dagre-GXQ25YYZ-nvgy6yju.js} +1 -1
  148. package/static/dist/assets/{diagram-S7CK7UJ4-itodc50c.js → diagram-S7CK7UJ4-g98nyv72.js} +1 -1
  149. package/static/dist/assets/{diagram-UQ7AKVKN-n1h2t4ah.js → diagram-UQ7AKVKN-ovgm6lvv.js} +1 -1
  150. package/static/dist/assets/{diagram-VSXAHHWV-lvtvm9bj.js → diagram-VSXAHHWV-pb2bn2e4.js} +1 -1
  151. package/static/dist/assets/{diagram-VX7I27RA-cgas58sy.js → diagram-VX7I27RA-6j4mvfqo.js} +1 -1
  152. package/static/dist/assets/{diagram-Z3DM3KII-d0aeps1q.js → diagram-Z3DM3KII-dqmb982r.js} +1 -1
  153. package/static/dist/assets/{ebnfDiagram-PWID7BFC-md345fyq.js → ebnfDiagram-PWID7BFC-ozbpzb6d.js} +1 -1
  154. package/static/dist/assets/{endpoints-e58saih3.js → endpoints-k8agnro8.js} +8 -8
  155. package/static/dist/assets/{erDiagram-RLTQ6QDP-i4rua63b.js → erDiagram-RLTQ6QDP-hl7m8vua.js} +1 -1
  156. package/static/dist/assets/eventmodeling-NTZA5JFV-j1y2ekec.js +1 -0
  157. package/static/dist/assets/flowDiagram-HODETNUW-g2cvcbm5.js +1 -0
  158. package/static/dist/assets/{ganttDiagram-EL5Y4UJY-geqx7zgl.js → ganttDiagram-EL5Y4UJY-ompixqmh.js} +1 -1
  159. package/static/dist/assets/{gitGraph-4MIJSDKK-leoq0z8t.js → gitGraph-4MIJSDKK-b6vxwp9q.js} +1 -1
  160. package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-llrlxfha.js → gitGraphDiagram-WWUBYQGX-k98f57on.js} +1 -1
  161. package/static/dist/assets/{highlighted-body-KPVGNVTW-kq2cnw6q.js → highlighted-body-KPVGNVTW-cd3ff6ge.js} +1 -1
  162. package/static/dist/assets/index-nfy0ezs6.css +2 -0
  163. package/static/dist/assets/{index-f9bzglkn.js → index-nqed3oum.js} +3 -3
  164. package/static/dist/assets/{info-A6RAGUB7-lkly4xj6.js → info-A6RAGUB7-hjcwjami.js} +1 -1
  165. package/static/dist/assets/{infoDiagram-27XIBGKW-oo7kdyte.js → infoDiagram-27XIBGKW-fm06rj36.js} +1 -1
  166. package/static/dist/assets/{ishikawaDiagram-5VMMS53U-hq6jf374.js → ishikawaDiagram-5VMMS53U-bxak4mwu.js} +1 -1
  167. package/static/dist/assets/{journeyDiagram-3NMN7TZE-is03ntb0.js → journeyDiagram-3NMN7TZE-khmihclr.js} +1 -1
  168. package/static/dist/assets/{kanban-definition-UXKFOSKX-jmerizqb.js → kanban-definition-UXKFOSKX-h1r32wdx.js} +1 -1
  169. package/static/dist/assets/{lazyRouteComponent-b1h2ykmb.js → lazyRouteComponent-b2eg0fws.js} +1 -1
  170. package/static/dist/assets/{line-laaa8h99.js → line-friyjof3.js} +1 -1
  171. package/static/dist/assets/{link-d8f0788e.js → link-m90ahx3n.js} +1 -1
  172. package/static/dist/assets/login-bw7m2fzo.js +1 -0
  173. package/static/dist/assets/mermaid-HWGCJPDP-njwcl62n.js +1 -0
  174. package/static/dist/assets/{mermaid-parser.core-ix4junux.js → mermaid-parser.core-f1gvpwrt.js} +3 -3
  175. package/static/dist/assets/{mindmap-definition-YA3MSWOX-o0e0adl1.js → mindmap-definition-YA3MSWOX-mt2oxwue.js} +1 -1
  176. package/static/dist/assets/{onboarding-lx0d6b38.js → onboarding-ret37j1e.js} +1 -1
  177. package/static/dist/assets/{packet-AYTQ26CC-kl8pwsyg.js → packet-AYTQ26CC-jik4ixlf.js} +1 -1
  178. package/static/dist/assets/{pegDiagram-XKGWAZYB-bbqn9jo4.js → pegDiagram-XKGWAZYB-erxlv6cc.js} +1 -1
  179. package/static/dist/assets/{pie-WAS4IAKB-cpyo8bkr.js → pie-WAS4IAKB-mnkoldfl.js} +1 -1
  180. package/static/dist/assets/{pieDiagram-E7YTZNPT-pelug34f.js → pieDiagram-E7YTZNPT-i7lwrada.js} +1 -1
  181. package/static/dist/assets/{quadrantDiagram-AXDQQJYC-jds4jmr5.js → quadrantDiagram-AXDQQJYC-fr8ejdoy.js} +1 -1
  182. package/static/dist/assets/{radar-RG4KPBEZ-e02oxnvd.js → radar-RG4KPBEZ-otarjte2.js} +1 -1
  183. package/static/dist/assets/{railroad-74A4TZTK-kz3delwo.js → railroad-74A4TZTK-lw362oto.js} +1 -1
  184. package/static/dist/assets/railroad-abnf-HS5TGJTU-m741owar.js +1 -0
  185. package/static/dist/assets/railroad-ebnf-LZEXJU2U-noa4fbia.js +1 -0
  186. package/static/dist/assets/railroad-peg-WCYAUIDC-o4bpw4nb.js +1 -0
  187. package/static/dist/assets/{railroadDiagram-O6MQD6OU-jgroj2ew.js → railroadDiagram-O6MQD6OU-esyn75pw.js} +1 -1
  188. package/static/dist/assets/{refresh-cw-k7yxjdn3.js → refresh-cw-k66keho6.js} +1 -1
  189. package/static/dist/assets/{requirementDiagram-BXWQKSXE-m4az5e0j.js → requirementDiagram-BXWQKSXE-cyhi5u29.js} +1 -1
  190. package/static/dist/assets/{sankeyDiagram-P5KCCOFB-vlryqiv3.js → sankeyDiagram-P5KCCOFB-jljpbo0h.js} +1 -1
  191. package/static/dist/assets/{saved-brdfjjnw.js → saved-do49si3v.js} +1 -1
  192. package/static/dist/assets/{search-gt2ueyvg.js → search-j5ki6a9o.js} +1 -1
  193. package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-fr8hrpql.js → sequenceDiagram-WJ2MYXX4-fz0dhlu7.js} +1 -1
  194. package/static/dist/assets/{settings_section_extensions_title-l0zodj0b.js → settings_section_extensions_title-b7lojn0o.js} +1 -1
  195. package/static/dist/assets/share._token-kg8w647b.js +2 -0
  196. package/static/dist/assets/{sliders-horizontal-gcgscv8r.js → sliders-horizontal-c4wya4dy.js} +1 -1
  197. package/static/dist/assets/{stateDiagram-D77RDMKH-bg548iyr.js → stateDiagram-D77RDMKH-kdnithdx.js} +1 -1
  198. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-k2wg2645.js +1 -0
  199. package/static/dist/assets/{swimlanes-42K2YHIH-hk5z3efh.js → swimlanes-42K2YHIH-esd6gmz3.js} +1 -1
  200. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-n5kvjtgr.js +8 -0
  201. package/static/dist/assets/tabBuild-bjmyvmn5.js +56 -0
  202. package/static/dist/assets/{timeline-definition-24CTP7MA-olvknr62.js → timeline-definition-24CTP7MA-g0qygccv.js} +1 -1
  203. package/static/dist/assets/todoStore-kjk5l0sy.js +1 -0
  204. package/static/dist/assets/{treeView-Q6P3EWNA-ovvjsxtf.js → treeView-Q6P3EWNA-ndchzkuu.js} +1 -1
  205. package/static/dist/assets/{treemap-WGGIJYW6-ggkipj59.js → treemap-WGGIJYW6-hwjxj0zz.js} +1 -1
  206. package/static/dist/assets/{untitled-c8ydw0bu.js → untitled-mbr6tuhu.js} +1 -1
  207. package/static/dist/assets/{useForm-kd759sjk.js → useForm-ocxu460f.js} +1 -1
  208. package/static/dist/assets/useLocale-fzy5jhg6.js +1 -0
  209. package/static/dist/assets/{useParams-katwvzil.js → useParams-lo9u33d1.js} +1 -1
  210. package/static/dist/assets/{useSearch-c70sgau1.js → useSearch-lq1mr090.js} +1 -1
  211. package/static/dist/assets/{vennDiagram-4TSXK5OY-hzld7zt7.js → vennDiagram-4TSXK5OY-gubovd1r.js} +1 -1
  212. package/static/dist/assets/{wardley-WFR3VGLG-hxkeoms8.js → wardley-WFR3VGLG-ir3om65j.js} +1 -1
  213. package/static/dist/assets/{wardleyDiagram-VM6X3IG4-ljh90mb1.js → wardleyDiagram-VM6X3IG4-ce0or1ie.js} +1 -1
  214. package/static/dist/assets/{xychartDiagram-S5SC5T6Z-mnuaes03.js → xychartDiagram-S5SC5T6Z-dexylcjg.js} +1 -1
  215. package/static/dist/index.html +1 -1
  216. package/static/dist/sw.js +1 -1
  217. package/static/dist/assets/AppShell-haqi9qzu.js +0 -1
  218. package/static/dist/assets/ChatPage-hjyu2g64.js +0 -22
  219. package/static/dist/assets/HubRoute-gkd8o1vg.js +0 -2
  220. package/static/dist/assets/Select-jld3yeni.js +0 -1
  221. package/static/dist/assets/SessionListPanel-d3qx4ric.js +0 -1
  222. package/static/dist/assets/_app-bnip2pjg.js +0 -1
  223. package/static/dist/assets/_app.ext._extensionId-egptzrfy.js +0 -1
  224. package/static/dist/assets/_app.index-vdz13ul5.js +0 -1
  225. package/static/dist/assets/_app.insights-lgjshsba.js +0 -1
  226. package/static/dist/assets/_app.kanban-c18cjrpz.js +0 -1
  227. package/static/dist/assets/_app.logs-ezhojgeg.js +0 -1
  228. package/static/dist/assets/_app.memory-i00paisv.js +0 -1
  229. package/static/dist/assets/_app.profiles-kd02bwjx.js +0 -1
  230. package/static/dist/assets/_app.settings-giaudkuk.js +0 -1
  231. package/static/dist/assets/_app.settings._section-k4qtlexk.js +0 -1
  232. package/static/dist/assets/_app.skills-bby2uvl8.js +0 -1
  233. package/static/dist/assets/_app.tasks-1kz9bosm.js +0 -2
  234. package/static/dist/assets/_app.todos-izjdbkm2.js +0 -1
  235. package/static/dist/assets/_app.workspaces-lfh2kw7s.js +0 -1
  236. package/static/dist/assets/architecture-7GRP2DOG-b4u9z1w3.js +0 -1
  237. package/static/dist/assets/channel-o77ioktg.js +0 -1
  238. package/static/dist/assets/classDiagram-ZZMXUADV-f5rvod26.js +0 -1
  239. package/static/dist/assets/classDiagram-v2-VYDZK3BY-f5rvod26.js +0 -1
  240. package/static/dist/assets/eventmodeling-NTZA5JFV-bn3fjsaq.js +0 -1
  241. package/static/dist/assets/flowDiagram-HODETNUW-li70d120.js +0 -1
  242. package/static/dist/assets/index-gkh7kum4.css +0 -2
  243. package/static/dist/assets/login-llkohrcg.js +0 -1
  244. package/static/dist/assets/mermaid-HWGCJPDP-id0ffk2r.js +0 -1
  245. package/static/dist/assets/railroad-abnf-HS5TGJTU-l13ped2z.js +0 -1
  246. package/static/dist/assets/railroad-ebnf-LZEXJU2U-c7rg3g4g.js +0 -1
  247. package/static/dist/assets/railroad-peg-WCYAUIDC-od0b8niq.js +0 -1
  248. package/static/dist/assets/share._token-oyjv046r.js +0 -2
  249. package/static/dist/assets/stateDiagram-v2-MP3YSRHH-j5v40qih.js +0 -1
  250. package/static/dist/assets/swimlanesDiagram-VR7AAH4N-grzefo9c.js +0 -8
  251. package/static/dist/assets/tabBuild-idliyw9f.js +0 -56
  252. package/static/dist/assets/todoStore-j4c2xq0b.js +0 -1
  253. package/static/dist/assets/useLocale-hds4xmja.js +0 -1
@@ -14,7 +14,7 @@ import { HttpFailure, markSessionTitleGenerated } from './service.js';
14
14
  import { StreamRegistry, SessionChannels } from './streams.js';
15
15
  import { PendingPrompts, clarifyReply } from './pending.js';
16
16
  import { RunJournal } from './journal.js';
17
- import { Session, titleFrom } from './session.js';
17
+ import { CONTEXT_USAGE_FIELDS, Session, titleFrom } from './session.js';
18
18
  import { buildActiveTurnToken, completedToolIndex, publicToolFrame, redactSessionData, redactString, withToolId } from '../redact.js';
19
19
  import { dict } from '../config/agent-config.js';
20
20
  import { ReasoningTitleTracker, reasoningEventPayload } from './reasoning-titles.js';
@@ -24,12 +24,15 @@ import { withSessionWireFlags } from './list.js';
24
24
  import { hydrateAnchorActivityScenes, turnTerminalState, withTurnIds } from './anchor.js';
25
25
  import { persistentStateChanges, persistentStateSnapshot } from './state-saved.js';
26
26
  import { maxIterationsFromConfig, maxTokensFromConfig, processWakeupMaxIterations, reasoningConfigFromConfig, webuiEphemeralSystemPrompt, workspaceSystemMessage } from './turn-context.js';
27
- import { agentSteerText, assistantReplyAddedAfterCurrentTurn, buildPartialMessage, checkpointTurnStart, extractToolCallsFromMessages, injectMaxIterationSummaryFallback, isContextCompressionMarker, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, sessionLacksFinalAssistantAnswer, splitThinkingFromContent, stoppedTurnContext, stripXmlToolCalls, toolOutcome, withAttachmentObjects, withToolCallOutcomes, workspaceContextPrefix } from './merge.js';
28
- import { fallbackTitleFromExchange, firstExchangeSnippets, isGenericFallbackTitle, latestExchangeSnippets, looksInvalidGeneratedTitle, sanitizeGeneratedTitle, titleLanguageMismatch, titlePrompts } from './titles.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';
28
+ import { mayBecomeSilentReply, turnOrigin, withBackgroundUpdates } from './background-updates.js';
29
+ import { fallbackTitleFromExchange, firstExchangeSnippets, isGenericFallbackTitle, latestExchangeSnippets, looksInvalidGeneratedTitle, looksLikeDefaultCliTitle, sanitizeGeneratedTitle, titleLanguageMismatch, titlePrompts } from './titles.js';
29
30
  import { str } from '../util.js';
30
31
  export const CHAT_LOCK_WAIT_SECONDS = 2;
31
32
  const IMAGE_MODE_TIMEOUT_MS = 15_000;
32
33
  const TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT = 80;
34
+ /** How long a user's message waits for a stopped background turn to unwind before its own turn is admitted. */
35
+ const BACKGROUND_UNWIND_WAIT_MS = 30_000;
33
36
  export function classifyProviderError(errStr, opts = {}) {
34
37
  const lower = errStr.toLowerCase();
35
38
  if (opts.condition === 'credential_missing')
@@ -148,6 +151,11 @@ export class TurnRunner {
148
151
  abortControllers = new Map();
149
152
  steers = new Map();
150
153
  consumedSteers = new Map();
154
+ /** Per stream: steer, withdraw and Send now run one at a time, so each sees the queue the Agent holds. */
155
+ steerChains = new Map();
156
+ /** Streams whose Agent steer queue a withdraw or Send now is rewriting right now, and the pending text seen meanwhile. */
157
+ steerRewrites = new Map();
158
+ deferredPendingSteer = new Map();
151
159
  lastCompletedTool = new Map();
152
160
  /** Streams whose run completed (`done` emitted) and only await title work; a late cancel is a no-op for these. */
153
161
  settledStreams = new Set();
@@ -158,6 +166,8 @@ export class TurnRunner {
158
166
  /** TAL-364: a Stop's in-flight `chat.interrupt` reply (null when it failed), so a worker that settles first can use its checkpoint. */
159
167
  interrupts = new Map();
160
168
  stopContexts = new Map();
169
+ /** Each turn's worker, so a stopped background turn can be awaited before the user's turn takes the session. */
170
+ workers = new Map();
161
171
  constructor(deps) {
162
172
  this.deps = deps;
163
173
  }
@@ -184,9 +194,10 @@ export class TurnRunner {
184
194
  return { error: 'session already has an active stream', active_stream_id: blocking, _status: 409 };
185
195
  }
186
196
  const streamId = randomUUID().replace(/-/g, '');
187
- const wasHiddenEmpty = !s.messages.length && !s.context_messages.length && !s.pending_user_message;
197
+ const wasHiddenEmpty = !s.messages.length && !s.context_messages.length && !s.hasPendingPrompt;
188
198
  const attachments = opts.attachments ?? [];
189
- const source = str(s.source_tag).toLowerCase() === 'fork' ? 'fork' : (opts.source ?? 'webui');
199
+ // A background wakeup stays one in a forked session too (TAL-460).
200
+ const source = opts.source !== 'process_wakeup' && str(s.source_tag).toLowerCase() === 'fork' ? 'fork' : (opts.source ?? 'webui');
190
201
  s.workspace = opts.workspace;
191
202
  s.model = opts.model;
192
203
  s.model_provider = opts.modelProvider;
@@ -214,10 +225,11 @@ export class TurnRunner {
214
225
  const channel = this.registry.create(streamId, s.session_id);
215
226
  if (opts.goalRelated)
216
227
  this.registry.goalRelated.add(streamId);
217
- this.registry.registerActiveRun({ stream_id: streamId, session_id: s.session_id, started_at: this.deps.now(), phase: 'starting', workspace: opts.workspace, model: opts.model, provider: opts.modelProvider, ephemeral: Boolean(opts.ephemeral) });
218
- void this.run(s.session_id, streamId, channel, opts).catch((error) => {
228
+ this.registry.registerActiveRun({ stream_id: streamId, session_id: s.session_id, started_at: this.deps.now(), phase: 'starting', workspace: opts.workspace, model: opts.model, provider: opts.modelProvider, ephemeral: Boolean(opts.ephemeral), origin: turnOrigin(source) });
229
+ const worker = this.run(s.session_id, streamId, channel, opts).catch((error) => {
219
230
  this.deps.log(`[webui] ERROR turn ${streamId} crashed\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
220
- });
231
+ }).finally(() => { this.workers.delete(streamId); });
232
+ this.workers.set(streamId, worker);
221
233
  const response = { stream_id: streamId, session_id: s.session_id, pending_started_at: s.pending_started_at, turn_id: streamId, title: s.title };
222
234
  if (opts.normalizedModel && opts.model)
223
235
  response.effective_model = opts.model;
@@ -246,7 +258,7 @@ export class TurnRunner {
246
258
  const put = (event, data, meta = {}) => {
247
259
  for (const steerEvent of this.takeSteerEventsBefore(streamId, event))
248
260
  put(steerEvent[0], steerEvent[1]);
249
- if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'pending_steer_leftover'].includes(event))
261
+ if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn'].includes(event))
250
262
  return;
251
263
  // `cancel()` already wrote the terminal row and closed the stream: the worker's unwind adds no second one.
252
264
  if (event === 'cancel' && !this.registry.streams.has(streamId)) {
@@ -290,11 +302,7 @@ export class TurnRunner {
290
302
  }
291
303
  const msgText = opts.msg;
292
304
  const previousMessages = structuredClone(s.messages);
293
- // Python `reconciled_state_db_messages_for_session(prefer_context=True)`: the model history is the owner context
294
- // extended append-only with the Agent's state.db rows (a CLI continuation of this session reaches the model), except
295
- // for a compressed context whose anchor cannot be verified — that stays context-only.
296
- const localContext = s.context_messages.length ? s.context_messages : s.messages.filter((m) => !m._error && !m._partial);
297
- const previousContext = structuredClone(localContext.some((m) => isContextCompressionMarker(m)) ? localContext : deps.service().mergedTranscript(s, localContext));
305
+ const previousContext = structuredClone(deps.service().modelContext(s));
298
306
  // Python `_sanitize_messages_for_api`: the model never sees display-only rows or a replayed cancelled prompt.
299
307
  const apiHistory = sanitizeMessagesForApi(previousContext);
300
308
  const workspaceCtx = workspaceContextPrefix(opts.workspace);
@@ -314,6 +322,11 @@ export class TurnRunner {
314
322
  const toolStartedAt = new WeakMap();
315
323
  let tokenSent = false;
316
324
  let firstTokenAt = null;
325
+ // TAL-460: a background turn's text is held back while it could still be a silence marker, so one never flashes by.
326
+ // ponytail: checks the turn's whole text, so a marker after earlier prose streams until the settled row hides it;
327
+ // check the last text segment instead if that shows up.
328
+ const holdSilence = turnOrigin(opts.source) === 'background';
329
+ let heldText = '';
317
330
  const titles = new ReasoningTitleTracker();
318
331
  let capturedTerminalError = null;
319
332
  const controller = new AbortController();
@@ -327,6 +340,8 @@ export class TurnRunner {
327
340
  activeRun.phase = 'running';
328
341
  const settledAt = { value: false };
329
342
  let failed = false;
343
+ // TAL-424: steers an errored turn ended without taking, sent as one follow-up turn once this one is torn down.
344
+ let followUp = [];
330
345
  try {
331
346
  if (!sidecar)
332
347
  throw new SidecarError('The Agent sidecar is not running; chat is unavailable until it starts.', { condition: 'sidecar_unavailable' });
@@ -349,7 +364,7 @@ export class TurnRunner {
349
364
  const frozenWorkspace = str(s.created_workspace) || str(s.workspace);
350
365
  const turnContext = {
351
366
  system_message: workspaceSystemMessage(frozenWorkspace),
352
- ephemeral_system_prompt: webuiEphemeralSystemPrompt({ config: cfg, personality: str(s.personality) || null, sessionId, profile: s.profile ?? null, workspace: frozenWorkspace, hermesHome: deps.profileHome(s.profile), homeDisplay: deps.homeDisplay?.() ?? '~/.hermes' }),
367
+ ephemeral_system_prompt: webuiEphemeralSystemPrompt({ config: cfg, personality: str(s.personality) || null, sessionId, profile: s.profile ?? null, workspace: frozenWorkspace, hermesHome: deps.profileHome(s.profile), homeDisplay: deps.homeDisplay?.() ?? '~/.hermes', background: turnOrigin(opts.source) === 'background' }),
353
368
  max_iterations: processWakeupMaxIterations(maxIterationsFromConfig(cfg), opts.source ?? 'webui', deps.env ?? {}),
354
369
  max_tokens: maxTokensFromConfig(cfg),
355
370
  reasoning_config: reasoningConfigFromConfig(cfg, opts.modelProvider ?? s.model_provider ?? null),
@@ -375,7 +390,12 @@ export class TurnRunner {
375
390
  if (stable !== null)
376
391
  put('reasoning', { text: '', titles: stable });
377
392
  partialText.push(str(data.text));
378
- put('token', { text: str(data.text) });
393
+ if (holdSilence && mayBecomeSilentReply(partialText.join(''))) {
394
+ heldText += str(data.text);
395
+ return;
396
+ }
397
+ put('token', { text: heldText + str(data.text) });
398
+ heldText = '';
379
399
  return;
380
400
  }
381
401
  case 'reasoning': {
@@ -387,9 +407,12 @@ export class TurnRunner {
387
407
  return;
388
408
  }
389
409
  case 'steer_pending':
390
- for (const record of this.takeConsumedSteers(streamId, str(data.text), { keepLeftovers: true }))
391
- put('steer_consumed', record);
392
- this.saveConsumedSteers(sessionId, streamId);
410
+ // TAL-424: while a withdraw or Send now rewrites the Agent's queue its pending text is ambiguous; it settles after.
411
+ if (this.steerRewrites.has(streamId)) {
412
+ this.deferredPendingSteer.set(streamId, str(data.text));
413
+ return;
414
+ }
415
+ this.settleConsumedSteers(sessionId, streamId, str(data.text));
393
416
  return;
394
417
  case 'tool': {
395
418
  const call = { name: data.name, args: data.args ?? {}, tid: str(data.tid), done: false };
@@ -494,7 +517,7 @@ export class TurnRunner {
494
517
  // sidecar reports `completed` whenever a failed run still carries messages).
495
518
  // Python's second chance: a turn that emitted no new row still counts when the merged transcript it produced
496
519
  // ends on a final answer (the current user row or trailing tool activity makes it "lacking").
497
- const mergedForCheck = () => mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId });
520
+ const mergedForCheck = () => mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] });
498
521
  const assistantAdded = assistantReplyAddedAfterCurrentTurn(resultMessages, previousContext, msgText) || !sessionLacksFinalAssistantAnswer(mergedForCheck());
499
522
  const lastErr = result.error ?? capturedTerminalError ?? '';
500
523
  // Python `_turn_transcript_lacks_final_assistant_answer`: a partial result with no final answer is a silent failure even if tokens streamed.
@@ -503,8 +526,11 @@ export class TurnRunner {
503
526
  const classification = classifyProviderError(lastErr, { silentFailure: !lastErr });
504
527
  const errStr = lastErr || `${classification.label}.`;
505
528
  const payload = providerErrorPayload(errStr, classification.type, classification.hint, deps.redactEnabled());
506
- // Settle the steers first so the persisted turn carries every consumed one; the Agent's pending text stays a leftover.
507
- const steerEvents = this.finalizeSteerEvents(streamId, str(result.pending_steer));
529
+ // Settle the steers first so the persisted turn carries every consumed one; the Agent's pending text is sent next.
530
+ // A withdraw or Send now still in flight finishes first, so the settlement sees the queue it left.
531
+ await this.steerRewrites.get(streamId);
532
+ const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, str(result.pending_steer), 'followup');
533
+ followUp = leftovers;
508
534
  this.persistError(s, streamId, classification.label, payload, activeTurnToken);
509
535
  payload.session = redactSessionData(this.terminalSessionPayload(s), deps.redactEnabled());
510
536
  payload.session_id = s.session_id;
@@ -517,8 +543,9 @@ export class TurnRunner {
517
543
  }
518
544
  // ── settle the transcript ──
519
545
  // The Agent's last pending-steer text settles the remaining steers before the turn is written back.
520
- const steerEvents = this.finalizeSteerEvents(streamId, result.pending_steer);
521
- s.messages = mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId });
546
+ await this.steerRewrites.get(streamId);
547
+ 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 ?? [] });
522
549
  s.context_messages = dedupeContext(resultMessages);
523
550
  for (const m of s.messages) {
524
551
  if (m.role !== 'assistant')
@@ -567,7 +594,8 @@ export class TurnRunner {
567
594
  if (m.role === 'user') {
568
595
  const content = messageText(m.content);
569
596
  const base = msgText.includes('\n\n[Attached files:') ? (msgText.split('\n\n[Attached files:')[0] ?? '').trim() : msgText;
570
- if (content.includes(base.slice(0, 60)) || msgText.includes(content.slice(0, 60)))
597
+ // An attachment-only prompt has no text to match: only this turn's own row takes its files.
598
+ if (msgText ? content.includes(base.slice(0, 60)) || msgText.includes(content.slice(0, 60)) : m._turn_id === streamId)
571
599
  m.attachments = [...attachments];
572
600
  break;
573
601
  }
@@ -635,6 +663,10 @@ export class TurnRunner {
635
663
  cache_hit_percent: cacheHit(cacheReadTokens, inputTokens), turn_cache_hit_percent: cacheHit(turnCacheRead, turnInput), duration_seconds: Math.round(duration * 1000) / 1000,
636
664
  context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
637
665
  };
666
+ const doneSession = redactSessionData(this.terminalSessionPayload(s), deps.redactEnabled());
667
+ // TAL-299: the ring's figures match the terminal session's (and a detail reload's).
668
+ for (const key of CONTEXT_USAGE_FIELDS)
669
+ doneUsage[key] = doneSession[key];
638
670
  if (usage.completion_tokens && duration > 0)
639
671
  doneUsage.tps = Math.round((usage.completion_tokens / duration) * 10) / 10;
640
672
  if (firstTokenAt !== null)
@@ -643,7 +675,7 @@ export class TurnRunner {
643
675
  if (usedModel)
644
676
  doneUsage.used_model = usedModel;
645
677
  const donePayload = {
646
- session: redactSessionData(this.terminalSessionPayload(s), deps.redactEnabled()),
678
+ session: doneSession,
647
679
  usage: doneUsage,
648
680
  terminal_state: turnTerminalState(s.messages, streamId),
649
681
  };
@@ -657,6 +689,7 @@ export class TurnRunner {
657
689
  // worker before its daemon-thread title generation).
658
690
  this.registry.activeRuns.delete(streamId);
659
691
  this.settledStreams.add(streamId);
692
+ this.startSteerFollowUp(sessionId, leftovers);
660
693
  await this.backgroundTitle(s, put);
661
694
  // Python: the last non-error assistant reply with content, else "(no answer produced)".
662
695
  if (opts.onDone) {
@@ -720,21 +753,26 @@ export class TurnRunner {
720
753
  // not race the error writeback above (a re-saved session would resurface in the sidebar).
721
754
  if (failed)
722
755
  opts.onFailed?.();
756
+ this.startSteerFollowUp(sessionId, followUp);
723
757
  }
724
758
  }
725
759
  /**
726
760
  * Python `_build_user_message`: image attachments are embedded as native `image_url` parts only when the Agent's
727
761
  * resolved image mode for this model is `native` (text mode routes them through the Agent's vision tool path), and
728
- * only after the bytes are read through an anchored descriptor and sniffed as a real image format.
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.
729
765
  */
730
766
  async buildUserMessage(workspaceCtx, msgText, attachments, workspace, sessionId, s, opts, signal) {
731
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);
732
770
  const candidates = attachments.filter((att) => str(att.path).trim() && str(att.mime).trim().startsWith('image/'));
733
771
  if (!candidates.length)
734
- return text;
772
+ return withFiles();
735
773
  const sidecar = this.deps.sidecar();
736
774
  if (!sidecar)
737
- return text;
775
+ return withFiles();
738
776
  // A cancel that landed before this point is final: never start the lookup or wait on it.
739
777
  if (signal.aborted)
740
778
  return text;
@@ -750,15 +788,15 @@ export class TurnRunner {
750
788
  if (mode.mode !== 'native') {
751
789
  const cfg = (await this.deps.profileConfig?.(s.profile ?? null)) ?? {};
752
790
  if (explicitTextSignal(cfg) || mode.supports_vision === false)
753
- return text;
791
+ return withFiles();
754
792
  }
755
793
  }
756
794
  catch (error) {
757
795
  if (!signal.aborted)
758
796
  this.deps.log(`[webui] image mode lookup failed for ${sessionId}: ${error.message}`);
759
- return text;
797
+ return withFiles();
760
798
  }
761
- const parts = [{ type: 'text', text }];
799
+ const parts = [];
762
800
  let images = 0;
763
801
  const roots = [workspace, this.deps.attachmentDir(sessionId)].map((r) => resolvePathLikePython(r));
764
802
  for (const att of candidates) {
@@ -790,15 +828,16 @@ export class TurnRunner {
790
828
  closeSync(fd);
791
829
  }
792
830
  }
793
- return images ? parts : text;
831
+ return images ? [{ type: 'text', text: withFiles() }, ...parts] : withFiles();
794
832
  }
795
833
  /**
796
834
  * Python `_session_payload_with_terminal_window`: the same visible-message window contract as `GET /api/session?
797
835
  * msg_limit=` (renderable rows, limited payload shape, windowed tool calls, todo state) with the full count.
798
836
  */
799
837
  terminalSessionPayload(s) {
800
- const payload = withSessionWireFlags(s.compact(), this.registry.liveIds);
801
- const scened = hydrateAnchorActivityScenes(withToolCallOutcomes(withTurnIds(withAttachmentObjects(s.messages)), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, clipToolResults: true });
838
+ const payload = withSessionWireFlags(s.compact({ contextLengthFor: this.deps.service().deps.contextLengthFor }), this.registry.liveIds);
839
+ 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);
802
841
  const [window, offset] = messageWindowForDisplay(scened, TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT, null);
803
842
  const limited = messagesForLimitedPayload(window);
804
843
  payload.messages = limited;
@@ -853,7 +892,7 @@ export class TurnRunner {
853
892
  }
854
893
  materializePendingUserTurn(s, activeTurnToken, turnId) {
855
894
  const pendingText = str(s.pending_user_message);
856
- if (!pendingText)
895
+ if (!s.hasPendingPrompt)
857
896
  return false;
858
897
  const recoveredTs = typeof s.pending_started_at === 'number' && s.pending_started_at > 0 ? s.pending_started_at : this.deps.now();
859
898
  const source = s.pending_user_source ?? 'webui';
@@ -932,6 +971,11 @@ export class TurnRunner {
932
971
  this.deps.store.sessions.delete(current.session_id);
933
972
  return true;
934
973
  }
974
+ // TAL-460: a background turn stops quietly: its prompt and model context are kept, but no partial and no Stop row,
975
+ // and its first settlement is final.
976
+ const quiet = this.registry.activeRuns.get(streamId)?.origin === 'background';
977
+ if (quiet && current.active_stream_id === null)
978
+ return true;
935
979
  const stop = this.stopContexts.get(streamId);
936
980
  const canonical = stop !== undefined && checkpoint !== null && checkpointTurnStart(checkpoint, stop.msgText, stop.historyLength) !== null;
937
981
  const settle = () => {
@@ -967,8 +1011,10 @@ export class TurnRunner {
967
1011
  current.pending_attachments = [];
968
1012
  current.pending_started_at = null;
969
1013
  current.pending_user_source = null;
970
- this.appendPartialSnapshot(current, streamId);
971
- current.messages.push({ role: 'assistant', content: '', _error: true, _terminal_state: 'cancelled', timestamp: Math.trunc(this.deps.now()), _turn_id: streamId });
1014
+ if (!quiet) {
1015
+ this.appendPartialSnapshot(current, streamId);
1016
+ current.messages.push({ role: 'assistant', content: '', _error: true, _terminal_state: 'cancelled', timestamp: Math.trunc(this.deps.now()), _turn_id: streamId });
1017
+ }
972
1018
  this.persistConsumedSteers(current, streamId, startedAt, this.deps.now());
973
1019
  try {
974
1020
  this.deps.store.save(current);
@@ -1077,7 +1123,9 @@ export class TurnRunner {
1077
1123
  const sessionId = s.session_id;
1078
1124
  const placeholder = str(s.title).trim();
1079
1125
  const invalidExisting = looksInvalidGeneratedTitle(s.title);
1080
- const eligible = ['Untitled', 'New Chat', ''].includes(placeholder) || placeholder === titleFrom(s.messages, '') || invalidExisting;
1126
+ // A claimed CLI session still carrying its default `<Source> Session` title is retitled like a new chat (TAL-256).
1127
+ const defaultTitle = (session) => looksLikeDefaultCliTitle({ title: session.title, source_tag: session.source_tag, raw_source: session.raw_source, session_source: session.session_source, source_label: session.source_label });
1128
+ const eligible = ['Untitled', 'New Chat', ''].includes(placeholder) || placeholder === titleFrom(s.messages, '') || invalidExisting || defaultTitle(s);
1081
1129
  const [userText, assistantText] = firstExchangeSnippets(s.messages);
1082
1130
  // Python `_put_title_status`: empty reason/title/raw_preview keys are omitted.
1083
1131
  const status = (status, reason = '', title = '', rawPreview = '') => {
@@ -1116,7 +1164,7 @@ export class TurnRunner {
1116
1164
  let effective = str(current.title).trim();
1117
1165
  let wrote = false;
1118
1166
  if (next) {
1119
- const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title);
1167
+ const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title) || defaultTitle(current);
1120
1168
  if (current.manual_title || !stillAuto) {
1121
1169
  status('skipped', 'manual_title', effective);
1122
1170
  return;
@@ -1144,16 +1192,19 @@ export class TurnRunner {
1144
1192
  }
1145
1193
  }
1146
1194
  // ── cancel / steer ───────────────────────────────────────────────────────
1147
- /** Python `cancel_stream`: persist the partial, mark cancelled, interrupt the Agent, release admission. */
1195
+ /**
1196
+ * Python `cancel_stream`: persist the partial, mark cancelled, interrupt the Agent, release admission. `withdrawn` is the
1197
+ * `steer_withdrawn` (stopped) this Stop emitted, for the cancel response (TAL-426).
1198
+ */
1148
1199
  async cancel(streamId) {
1149
1200
  const channel = this.registry.peek(streamId);
1150
1201
  const run = this.registry.activeRuns.get(streamId);
1151
1202
  if (!channel && !run)
1152
- return false;
1203
+ return { cancelled: false, withdrawn: [] };
1153
1204
  // Python popped the run before its title work: a cancel that lands after `done` (while the title prompts still run)
1154
1205
  // is not a cancellation, so it neither journals a `cancel` frame nor marks the completed run interrupted.
1155
1206
  if (!run && this.settledStreams.has(streamId))
1156
- return false;
1207
+ return { cancelled: false, withdrawn: [] };
1157
1208
  const sessionId = this.registry.ownerSessionId(streamId) ?? run?.session_id ?? null;
1158
1209
  this.registry.cancelled.add(streamId);
1159
1210
  if (run) {
@@ -1174,7 +1225,9 @@ export class TurnRunner {
1174
1225
  const leftover = str(reply?.pending_steer);
1175
1226
  const checkpoint = reply?.checkpoint ?? null;
1176
1227
  // Settle the steers first so the persisted cancel carries every consumed one.
1177
- const steerEvents = (this.steers.get(streamId) ?? []).length ? this.finalizeSteerEvents(streamId, leftover) : [];
1228
+ // TAL-424: a Stop withdraws the steers the Agent never took, with their text, and starts nothing after.
1229
+ await this.steerRewrites.get(streamId);
1230
+ const steerEvents = (this.steers.get(streamId) ?? []).length ? this.finalizeSteers(streamId, leftover, 'stopped').events : [];
1178
1231
  if (sessionId) {
1179
1232
  let current = null;
1180
1233
  try {
@@ -1210,6 +1263,21 @@ export class TurnRunner {
1210
1263
  this.registry.liveIds.delete(streamId);
1211
1264
  }
1212
1265
  this.abortControllers.get(streamId)?.abort();
1266
+ const withdrawn = steerEvents.filter(([event]) => event === 'steer_withdrawn').map(([, data]) => data);
1267
+ return { cancelled: true, withdrawn };
1268
+ }
1269
+ /**
1270
+ * TAL-460: a user's message never joins a background turn. That turn stops quietly (`finalizeCancelled`) and its worker
1271
+ * unwinds before the user's turn is admitted, so two Agent runs never share the session. True when one was stopped.
1272
+ */
1273
+ async yieldBackgroundTurn(sessionId) {
1274
+ const streamId = this.registry.activeRunStreamForSession(sessionId);
1275
+ if (!streamId || this.registry.activeRuns.get(streamId)?.origin !== 'background')
1276
+ return false;
1277
+ await this.cancel(streamId);
1278
+ let timer;
1279
+ await Promise.race([this.workers.get(streamId), new Promise((resolve) => { timer = setTimeout(resolve, BACKGROUND_UNWIND_WAIT_MS); })]);
1280
+ clearTimeout(timer);
1213
1281
  return true;
1214
1282
  }
1215
1283
  async steer(sessionId, text, displayText, steerId) {
@@ -1234,7 +1302,10 @@ export class TurnRunner {
1234
1302
  return { accepted: false, fallback: 'no_cached_agent', stream_id: null };
1235
1303
  // Register provisionally before the RPC: the Agent may apply the steer and finish the turn before the steer
1236
1304
  // reply arrives, and the turn's finalisation must then already see the record. A rejection removes it again.
1237
- const record = { steer_id: steerId, session_id: sessionId, stream_id: activeStreamId, text, display_text: displayText || text, created_at: this.deps.now() };
1305
+ return this.withSteerChain(activeStreamId, () => this.steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId));
1306
+ }
1307
+ async steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId) {
1308
+ const record = { steer_id: steerId, session_id: sessionId, stream_id: activeStreamId, text, display_text: displayText || text, created_at: this.deps.now(), state: 'pending', can_redirect: false };
1238
1309
  const records = this.steers.get(activeStreamId) ?? [];
1239
1310
  records.push(record);
1240
1311
  this.steers.set(activeStreamId, records);
@@ -1259,6 +1330,12 @@ export class TurnRunner {
1259
1330
  // An accepted steer whose turn finalised while the reply was in flight was already reported by that finalisation.
1260
1331
  if (!result.accepted)
1261
1332
  withdraw();
1333
+ else {
1334
+ record.can_redirect = result.can_redirect === true;
1335
+ // TAL-424: every client shows it until the Agent takes it (unless it already did while the reply was in flight).
1336
+ if (this.steers.get(activeStreamId)?.includes(record))
1337
+ this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
1338
+ }
1262
1339
  return { accepted: result.accepted, fallback: result.accepted ? null : (result.fallback ?? 'not_running'), stream_id: activeStreamId, steer_id: steerId };
1263
1340
  }
1264
1341
  /** Python `_webui_steer_events_before` for terminal frames: cancel/apperror flush consumed steers and leftovers first. */
@@ -1267,7 +1344,7 @@ export class TurnRunner {
1267
1344
  return [];
1268
1345
  if (!(this.steers.get(streamId) ?? []).length)
1269
1346
  return [];
1270
- return this.finalizeSteerEvents(streamId, '');
1347
+ return this.finalizeSteers(streamId, '', 'stopped').events;
1271
1348
  }
1272
1349
  /** Python `_pending_webui_steer_suffix_start`: the pending text is the newline-joined tail of the records; earlier ones are consumed. */
1273
1350
  static pendingSteerSuffixStart(records, pendingText) {
@@ -1280,15 +1357,15 @@ export class TurnRunner {
1280
1357
  }
1281
1358
  return null;
1282
1359
  }
1283
- consumedSteerPayload(record) {
1360
+ consumedSteerPayload(record, opts = {}) {
1284
1361
  const consumedAt = this.deps.now();
1285
1362
  const afterToolCallId = this.lastCompletedTool.get(record.stream_id) ?? null;
1286
1363
  const consumed = this.consumedSteers.get(record.stream_id) ?? [];
1287
1364
  if (!consumed.some((c) => c.steer_id === record.steer_id)) {
1288
- consumed.push({ steer_id: record.steer_id, text: record.display_text || record.text, agent_text: record.text, submitted_at: record.created_at, consumed_at: consumedAt, after_tool_call_id: afterToolCallId });
1365
+ consumed.push({ steer_id: record.steer_id, text: record.display_text || record.text, agent_text: record.text, submitted_at: record.created_at, consumed_at: consumedAt, after_tool_call_id: afterToolCallId, ...(opts.redirected ? { redirected: true } : {}) });
1289
1366
  this.consumedSteers.set(record.stream_id, consumed);
1290
1367
  }
1291
- return { ...record, agent_text: record.text, text: record.display_text || record.text, consumed_at: consumedAt, after_tool_call_id: afterToolCallId };
1368
+ return { steer_id: record.steer_id, session_id: record.session_id, stream_id: record.stream_id, display_text: record.display_text, created_at: record.created_at, agent_text: record.text, text: record.display_text || record.text, consumed_at: consumedAt, after_tool_call_id: afterToolCallId };
1292
1369
  }
1293
1370
  /**
1294
1371
  * Consumed steers become display-only `_steer` user rows at their causal place in the turn: after the tool results of
@@ -1309,7 +1386,14 @@ export class TurnRunner {
1309
1386
  };
1310
1387
  // An Agent that records the steer it delivered does so at its exact place: that row becomes the steer, not a copy.
1311
1388
  const agentRows = s.messages.flatMap((m, i) => (m._turn_id === streamId && agentSteerText(m) !== null ? [i] : []));
1312
- const agentRow = agentRows.find((i) => agentSteerText(s.messages[i]) === steer.agent_text.trim()) ?? agentRows[0];
1389
+ let agentRow = agentRows.find((i) => agentSteerText(s.messages[i]) === steer.agent_text.trim()) ?? agentRows[0];
1390
+ // A Send now redirect is recorded as a plain user row of the turn: that row becomes the steer too.
1391
+ if (steer.redirected) {
1392
+ const prompt = s.messages.findIndex((m) => m._turn_id === streamId && m.role === 'user' && !m._steer);
1393
+ const own = s.messages.findIndex((m, i) => i > prompt && m._turn_id === streamId && m.role === 'user' && !m._steer && messageText(m.content).trim() === steer.agent_text.trim());
1394
+ // Never another steer's row: without its own correction row (the Agent joins two redirects) it gets a new one.
1395
+ agentRow = own >= 0 ? own : undefined;
1396
+ }
1313
1397
  if (agentRow === undefined)
1314
1398
  s.messages.splice(steerInsertIndex(s.messages, streamId, steer.after_tool_call_id), 0, row);
1315
1399
  else
@@ -1364,22 +1448,181 @@ export class TurnRunner {
1364
1448
  return [];
1365
1449
  return records.slice(0, start).map((r) => this.consumedSteerPayload(r));
1366
1450
  }
1367
- /** Terminal steer bookkeeping for cancel/error paths: consumed records first, then `pending_steer_leftover` for the rest. */
1368
- finalizeSteerEvents(streamId, leftoverText) {
1451
+ /**
1452
+ * Terminal steer bookkeeping: the records the Agent no longer holds are consumed; the rest it never took are either
1453
+ * `stopped` (withdrawn with their text for the composer) or returned as one `followup` turn's texts (TAL-424).
1454
+ */
1455
+ finalizeSteers(streamId, leftoverText, mode) {
1369
1456
  const records = this.steers.get(streamId) ?? [];
1370
1457
  this.steers.delete(streamId);
1371
- const start = TurnRunner.pendingSteerSuffixStart(records, leftoverText);
1372
- if (start === null)
1373
- return leftoverText ? [['pending_steer_leftover', { stream_id: streamId, text: leftoverText }]] : [];
1374
- const events = records.slice(0, start).map((r) => ['steer_consumed', this.consumedSteerPayload(r)]);
1375
- const leftovers = records.slice(start);
1376
- for (const r of leftovers)
1377
- events.push(['pending_steer_leftover', { ...r, leftover_at: this.deps.now() }]);
1378
- const matched = leftovers.map((r) => r.text).join('\n');
1458
+ // Text that ends with none of these steers is another surface's: the Agent took every one of them.
1459
+ const start = TurnRunner.pendingSteerSuffixStart(records, leftoverText) ?? records.length;
1460
+ const consumed = records.slice(0, start);
1461
+ const left = records.slice(start);
1462
+ const matched = left.map((r) => r.text).join('\n');
1379
1463
  const unmatched = matched ? leftoverText.slice(0, leftoverText.length - matched.length).replace(/\n+$/, '') : leftoverText;
1464
+ const events = consumed.map((r) => ['steer_consumed', this.consumedSteerPayload(r)]);
1465
+ for (const r of left)
1466
+ events.push(['steer_withdrawn', { steer_id: r.steer_id, reason: mode, text: r.display_text }]);
1467
+ if (mode === 'followup')
1468
+ return { events, leftovers: [...(unmatched ? [unmatched] : []), ...left.map((r) => r.text)] };
1469
+ if (unmatched)
1470
+ events.push(['steer_withdrawn', { steer_id: null, reason: 'stopped', text: unmatched }]);
1471
+ // ponytail: legacy `pending_steer_leftover` keeps today's Web and App from losing a stopped steer (they requeue it);
1472
+ // drop it once TAL-425 / TAL-426 put `steer_withdrawn` text back in the composer.
1473
+ for (const r of left)
1474
+ events.push(['pending_steer_leftover', { steer_id: r.steer_id, stream_id: streamId, text: r.text, display_text: r.display_text, leftover_at: this.deps.now() }]);
1380
1475
  if (unmatched)
1381
1476
  events.push(['pending_steer_leftover', { stream_id: streamId, text: unmatched }]);
1382
- return events;
1477
+ return { events, leftovers: [] };
1478
+ }
1479
+ /**
1480
+ * TAL-424: the steers a finished turn never took go on as one follow-up turn, sent once by the server for every client.
1481
+ * Should another turn already hold the session, they steer into that one instead, so no text is lost.
1482
+ */
1483
+ startSteerFollowUp(sessionId, texts) {
1484
+ if (!texts.length)
1485
+ return;
1486
+ let s;
1487
+ try {
1488
+ s = this.deps.store.get(sessionId);
1489
+ }
1490
+ catch {
1491
+ return;
1492
+ }
1493
+ const msg = texts.join('\n');
1494
+ const started = this.start(s, { msg, attachments: [], workspace: s.workspace, model: s.model, modelProvider: s.model_provider });
1495
+ if (!started._status || started._status < 400)
1496
+ return;
1497
+ if (started._status === 409 && started.active_stream_id) {
1498
+ void this.steer(sessionId, msg, msg, `steer-${randomUUID()}`).then((r) => {
1499
+ if (r.accepted !== true)
1500
+ this.deps.log(`[webui] WARNING: steer follow-up for ${sessionId} was not accepted (${str(r.fallback)}); text: ${msg.slice(0, 200)}`);
1501
+ });
1502
+ return;
1503
+ }
1504
+ this.deps.log(`[webui] WARNING: steer follow-up for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
1505
+ }
1506
+ /** The Agent's pending steer text settles which steers it has taken: those become `steer_consumed` and their rows. */
1507
+ settleConsumedSteers(sessionId, streamId, pendingText) {
1508
+ for (const record of this.takeConsumedSteers(streamId, pendingText, { keepLeftovers: true }))
1509
+ this.emitToSession(sessionId, 'steer_consumed', record);
1510
+ this.saveConsumedSteers(sessionId, streamId);
1511
+ }
1512
+ /** What every client shows for a pending steer; the server decides which actions it offers. */
1513
+ pendingSteerView(r) {
1514
+ const open = r.state === 'pending';
1515
+ return { steer_id: r.steer_id, text: r.display_text, submitted_at: r.created_at, state: r.state, actions: { edit: open, cancel: open, send_now: open && r.can_redirect } };
1516
+ }
1517
+ /** TAL-424: the active stream's pending steers, oldest first (session detail `pending_steers`). */
1518
+ pendingSteers(streamId) {
1519
+ return (this.steers.get(streamId) ?? []).map((r) => this.pendingSteerView(r));
1520
+ }
1521
+ withSteerChain(streamId, fn) {
1522
+ const run = (this.steerChains.get(streamId) ?? Promise.resolve()).then(fn);
1523
+ const settled = run.catch(() => undefined).then(() => { if (this.steerChains.get(streamId) === settled)
1524
+ this.steerChains.delete(streamId); });
1525
+ this.steerChains.set(streamId, settled);
1526
+ return run;
1527
+ }
1528
+ /**
1529
+ * A withdraw or Send now on one pending steer of the session's active stream. `rewrite` asks the sidecar to change the
1530
+ * Agent's queue and says whether it did; pending text the Agent reported meanwhile is settled after, unless the queue
1531
+ * changed (the next report then reflects it).
1532
+ */
1533
+ async rewriteSteer(sessionId, steerId, fallback, rewrite) {
1534
+ let s = null;
1535
+ try {
1536
+ s = this.deps.store.get(sessionId, { metadataOnly: true });
1537
+ }
1538
+ catch {
1539
+ s = null;
1540
+ }
1541
+ const streamId = s?.active_stream_id;
1542
+ const sidecar = this.deps.sidecar();
1543
+ if (!streamId || !sidecar)
1544
+ return fallback;
1545
+ return this.withSteerChain(streamId, async () => {
1546
+ const records = this.steers.get(streamId) ?? [];
1547
+ const index = records.findIndex((r) => r.steer_id === steerId);
1548
+ if (index < 0 || records[index].state !== 'pending')
1549
+ return fallback;
1550
+ // In flight before the sidecar call, so a pending-text report racing its reply is deferred too.
1551
+ let finished = () => undefined;
1552
+ this.steerRewrites.set(streamId, new Promise((resolve) => { finished = resolve; }));
1553
+ let outcome = { result: fallback, changed: false };
1554
+ try {
1555
+ outcome = await rewrite(sidecar, streamId, records, index);
1556
+ }
1557
+ catch {
1558
+ outcome = { result: fallback, changed: false };
1559
+ }
1560
+ finally {
1561
+ this.steerRewrites.delete(streamId);
1562
+ finished();
1563
+ const deferred = this.deferredPendingSteer.get(streamId);
1564
+ this.deferredPendingSteer.delete(streamId);
1565
+ if (deferred !== undefined && !outcome.changed)
1566
+ this.settleConsumedSteers(sessionId, streamId, deferred);
1567
+ }
1568
+ return outcome.result;
1569
+ });
1570
+ }
1571
+ dropSteer(streamId, record) {
1572
+ const records = this.steers.get(streamId);
1573
+ const index = records?.indexOf(record) ?? -1;
1574
+ if (records && index >= 0)
1575
+ records.splice(index, 1);
1576
+ }
1577
+ /**
1578
+ * TAL-424 Edit and Cancel: take a pending steer back before the Agent takes it. `withdrawn: false` (and nothing
1579
+ * emitted) when it is unknown, already taken or being sent; a taken one then settles as consumed.
1580
+ */
1581
+ withdrawSteer(sessionId, steerId, reason) {
1582
+ return this.rewriteSteer(sessionId, steerId, { withdrawn: false }, async (sidecar, streamId, records, index) => {
1583
+ const record = records[index];
1584
+ const reply = await sidecar.call('chat.steer_withdraw', { stream_id: streamId, pending: records.map((r) => r.text), index });
1585
+ if (!reply.withdrawn)
1586
+ return { result: { withdrawn: false }, changed: false };
1587
+ this.dropSteer(streamId, record);
1588
+ this.emitToSession(sessionId, 'steer_withdrawn', { steer_id: record.steer_id, reason, text: record.display_text });
1589
+ return { result: { withdrawn: true, text: record.display_text }, changed: true };
1590
+ });
1591
+ }
1592
+ /**
1593
+ * TAL-424 Send now: deliver a pending steer with the Agent's redirect. A model request restarts with it (consumed at
1594
+ * once); during tools it goes last on the Agent's queue and stays `sending_now` until taken; with nothing live it
1595
+ * stays pending.
1596
+ */
1597
+ sendSteerNow(sessionId, steerId) {
1598
+ return this.rewriteSteer(sessionId, steerId, { redirected: false }, async (sidecar, streamId, records, index) => {
1599
+ const record = records[index];
1600
+ if (!record.can_redirect)
1601
+ return { result: { redirected: false }, changed: false };
1602
+ const reply = await sidecar.call('chat.steer_now', { stream_id: streamId, pending: records.map((r) => r.text), index });
1603
+ if (!reply.withdrawn)
1604
+ return { result: { redirected: false }, changed: false };
1605
+ if (!reply.redirected) {
1606
+ // The Agent's queue changed meanwhile, so it went back last: clients follow the new order.
1607
+ if (reply.requeued === 'last') {
1608
+ this.dropSteer(streamId, record);
1609
+ records.push(record);
1610
+ this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
1611
+ }
1612
+ return { result: { redirected: false }, changed: true };
1613
+ }
1614
+ this.dropSteer(streamId, record);
1615
+ if (reply.delivery === 'steer') {
1616
+ record.state = 'sending_now';
1617
+ records.push(record);
1618
+ this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
1619
+ }
1620
+ else {
1621
+ this.emitToSession(sessionId, 'steer_consumed', this.consumedSteerPayload(record, { redirected: true }));
1622
+ this.saveConsumedSteers(sessionId, streamId);
1623
+ }
1624
+ return { result: { redirected: true }, changed: true };
1625
+ });
1383
1626
  }
1384
1627
  // ── approvals / clarify ──────────────────────────────────────────────────
1385
1628
  async respondApproval(sessionId, choice, approvalId, enableYolo) {