@maudecode/talaria-web 1.14.0 → 1.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_release.json +4 -4
- package/dist/api/automation-router.d.ts +123 -7
- package/dist/api/automation-router.js +5 -0
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +728 -55
- package/dist/api/chat-router.js +122 -126
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.js +44 -32
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.js +5 -58
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +6 -3
- package/dist/api/router.js +10 -8
- package/dist/api/router.js.map +1 -1
- package/dist/api/sessions-router.d.ts +2583 -297
- package/dist/api/sessions-router.js +92 -47
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +188 -17
- package/dist/api/settings-router.js +75 -26
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.js +1 -1
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-router.d.ts +13 -1
- package/dist/api/tools-router.js +10 -8
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +3620 -343
- package/dist/auth/gate.js +8 -4
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/oidc.d.ts +6 -2
- package/dist/auth/oidc.js +16 -11
- package/dist/auth/oidc.js.map +1 -1
- package/dist/auth/store.d.ts +14 -2
- package/dist/auth/store.js +26 -1
- package/dist/auth/store.js.map +1 -1
- package/dist/cli/dotenv.d.ts +1 -1
- package/dist/cli/dotenv.js +36 -3
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/config/agent-config.js +20 -15
- package/dist/config/agent-config.js.map +1 -1
- package/dist/http/addresses.d.ts +6 -0
- package/dist/http/addresses.js +26 -0
- package/dist/http/addresses.js.map +1 -1
- package/dist/http/context.d.ts +7 -5
- package/dist/http/context.js.map +1 -1
- package/dist/links.d.ts +26 -0
- package/dist/links.js +60 -0
- package/dist/links.js.map +1 -0
- package/dist/onboarding.d.ts +1 -1
- package/dist/onboarding.js +28 -8
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.js +1 -1
- package/dist/profiles/profiles.js.map +1 -1
- package/dist/providers/catalog.d.ts +85 -1
- package/dist/providers/catalog.js +280 -43
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/tables.d.ts +3 -1
- package/dist/providers/tables.js +9 -4
- package/dist/providers/tables.js.map +1 -1
- package/dist/redact.js +493 -13
- package/dist/redact.js.map +1 -1
- package/dist/runtime.js +104 -42
- package/dist/runtime.js.map +1 -1
- package/dist/sessions/anchor.d.ts +22 -3
- package/dist/sessions/anchor.js +87 -42
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-tasks.d.ts +140 -0
- package/dist/sessions/background-tasks.js +352 -0
- package/dist/sessions/background-tasks.js.map +1 -0
- package/dist/sessions/background-updates.d.ts +56 -0
- package/dist/sessions/background-updates.js +152 -0
- package/dist/sessions/background-updates.js.map +1 -0
- package/dist/sessions/cli-sessions.d.ts +6 -0
- package/dist/sessions/cli-sessions.js +20 -7
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +17 -1
- package/dist/sessions/completions.js +84 -13
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/compress.d.ts +33 -0
- package/dist/sessions/compress.js +66 -0
- package/dist/sessions/compress.js.map +1 -0
- package/dist/sessions/list.d.ts +22 -4
- package/dist/sessions/list.js +113 -33
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +81 -7
- package/dist/sessions/merge.js +391 -26
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/pending.d.ts +5 -0
- package/dist/sessions/pending.js +30 -10
- package/dist/sessions/pending.js.map +1 -1
- package/dist/sessions/relay.d.ts +7 -1
- package/dist/sessions/relay.js +17 -4
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +118 -6
- package/dist/sessions/service.js +564 -65
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +22 -1
- package/dist/sessions/session.js +29 -2
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/shares.js +11 -2
- package/dist/sessions/shares.js.map +1 -1
- package/dist/sessions/source-kind.d.ts +22 -0
- package/dist/sessions/source-kind.js +54 -0
- package/dist/sessions/source-kind.js.map +1 -0
- package/dist/sessions/state-db.d.ts +10 -1
- package/dist/sessions/state-db.js +48 -17
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/store.d.ts +2 -0
- package/dist/sessions/store.js +24 -5
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +3 -0
- package/dist/sessions/streams.js +0 -5
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/titles.d.ts +4 -0
- package/dist/sessions/titles.js +16 -0
- package/dist/sessions/titles.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +1 -0
- package/dist/sessions/turn-context.js +3 -0
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +85 -7
- package/dist/sessions/turn.js +518 -170
- package/dist/sessions/turn.js.map +1 -1
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +13 -0
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/fake.js +2 -0
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/tools/commands.d.ts +15 -0
- package/dist/tools/commands.js +52 -0
- package/dist/tools/commands.js.map +1 -0
- package/dist/tools/crons.d.ts +38 -4
- package/dist/tools/crons.js +323 -36
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/mcp.js +5 -1
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +1 -1
- package/dist/tools/update-notifications.js +1 -1
- package/dist/tools/update-notifications.js.map +1 -1
- package/dist/tools/updates.d.ts +8 -3
- package/dist/tools/updates.js +133 -34
- package/dist/tools/updates.js.map +1 -1
- package/dist/workspace/git.d.ts +57 -10
- package/dist/workspace/git.js +302 -195
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media-refs.d.ts +13 -0
- package/dist/workspace/media-refs.js +165 -0
- package/dist/workspace/media-refs.js.map +1 -0
- package/dist/workspace/media.d.ts +19 -0
- package/dist/workspace/media.js +61 -2
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/workspaces.d.ts +7 -1
- package/dist/workspace/workspaces.js +30 -21
- package/dist/workspace/workspaces.js.map +1 -1
- package/dist/workspace/worktrees.d.ts +2 -4
- package/dist/workspace/worktrees.js +28 -33
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +4 -2
- package/sidecar/scripts/replay_sidecar.py +3 -1
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/home.py +59 -2
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +19 -3
- package/sidecar/talaria_sidecar/methods/chat.py +360 -27
- package/sidecar/talaria_sidecar/methods/commands.py +46 -31
- package/sidecar/talaria_sidecar/methods/config.py +14 -10
- package/sidecar/talaria_sidecar/methods/cron.py +22 -14
- package/sidecar/talaria_sidecar/methods/oauth.py +439 -0
- package/sidecar/talaria_sidecar/methods/process.py +318 -0
- package/sidecar/talaria_sidecar/methods/profiles.py +5 -4
- package/sidecar/talaria_sidecar/methods/providers.py +161 -1
- package/sidecar/talaria_sidecar/methods/runtime.py +2 -0
- package/sidecar/talaria_sidecar/methods/text.py +1 -20
- package/sidecar/talaria_sidecar/methods/usage.py +7 -1
- package/static/dist/FILES.txt +124 -119
- package/static/dist/assets/AppShell-ef3bhu32.js +1 -0
- package/static/dist/assets/Button-nx37f4u1.js +1 -0
- package/static/dist/assets/ChatPage-cdcpykcg.js +25 -0
- package/static/dist/assets/Dialog-mec42hvb.js +1 -0
- package/static/dist/assets/HubRoute-ecb65zpr.js +2 -0
- package/static/dist/assets/{Markdown-emii55o0.js → Markdown-jqrx5es0.js} +125 -125
- package/static/dist/assets/RightPanel-d1lgvk4s.js +1 -0
- package/static/dist/assets/Select-h4ivl9qf.js +1 -0
- package/static/dist/assets/SessionListPanel-ewib046b.js +1 -0
- package/static/dist/assets/SettingsLayout-lx654cup.js +1 -0
- package/static/dist/assets/States-ehsb6lzo.js +1 -0
- package/static/dist/assets/Toaster-mch1pa3t.js +1 -0
- package/static/dist/assets/_app-m8d961ef.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-mp33hbzi.js +1 -0
- package/static/dist/assets/_app.index-dk6zp7e0.js +1 -0
- package/static/dist/assets/_app.insights-jt2fk4i1.js +1 -0
- package/static/dist/assets/_app.kanban-if4pubeu.js +1 -0
- package/static/dist/assets/_app.logs-mfaw9m6g.js +1 -0
- package/static/dist/assets/_app.memory-h3eaarb6.js +1 -0
- package/static/dist/assets/_app.profiles-ienktovs.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-m6nx9g8t.js → _app.session._sessionId-hh27wwu2.js} +1 -1
- package/static/dist/assets/_app.settings-dpoe0bzf.js +1 -0
- package/static/dist/assets/_app.settings._section-l468ynag.js +3 -0
- package/static/dist/assets/_app.skills-kcbncc8i.js +1 -0
- package/static/dist/assets/_app.tasks-occdrqhw.js +2 -0
- package/static/dist/assets/_app.todos-nnke2rcc.js +1 -0
- package/static/dist/assets/_app.workspaces-cdlatvwj.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-kejgxpil.js → abnfDiagram-VCTEODGH-oity9y4o.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-hj11333z.js +1 -0
- package/static/dist/assets/architectureDiagram-5GKGNRK7-ceaepnyr.js +36 -0
- package/static/dist/assets/back-eu64w89w.js +1 -0
- package/static/dist/assets/{blockDiagram-I7D4REHJ-l1466a7j.js → blockDiagram-I7D4REHJ-n79st4pj.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-gg3aq3sr.js → c4Diagram-7LVT6UL2-mi73akzk.js} +1 -1
- package/static/dist/assets/cancel-mzvkdjg8.js +1 -0
- package/static/dist/assets/channel-mbvmf2vv.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-oalgz5l8.js → chunk-4HAMMTFA-laat0g1q.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-babmzhdg.js → chunk-75Z2AOVW-0psy1tr2.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-lagu350c.js → chunk-DU6HZSFF-lw8uox8x.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-gjh2tqif.js → chunk-F27PBJKO-erassib3.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-ipdgja5c.js → chunk-GMAD6QVW-fz7rx619.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-j8wkn1at.js → chunk-GVQU2GXP-gdb0epb2.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-k00laqyk.js → chunk-IMKFNOWR-ea8t26qw.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-fju6xqj4.js → chunk-L3NEJ4N5-mb1gu9kz.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-bi6widqt.js → chunk-OSK3NFVY-eovmccvy.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-czr4tebv.js → chunk-P2QGCYS3-bo55zjo5.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-ktdhn67w.js → chunk-POPQ4Y6H-ja7ohe4h.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-ii4dsu0d.js → chunk-PWAF6VOD-e30g9nus.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-fpwk3xxp.js → chunk-SHT3W25Y-gbb798f7.js} +3 -3
- package/static/dist/assets/{chunk-SVP7TREG-k0jrvhei.js → chunk-SVP7TREG-h2iw8urm.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-nkqv9ot0.js → chunk-TICWLB2K-fd2cottz.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-o8r05k0n.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-o8r05k0n.js +1 -0
- package/static/dist/assets/{copied-gpchexqu.js → copied-h65fc3cx.js} +1 -1
- package/static/dist/assets/copy-e6977uwq.js +1 -0
- package/static/dist/assets/{cynefin-OW5HDTMX-lqxt8rs3.js → cynefin-OW5HDTMX-lbltyl5j.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-j9eqovf5.js → cynefinDiagram-5FMLGOSQ-lxhsqq4a.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-m9agobjm.js → dagre-GXQ25YYZ-gn4nnaln.js} +4 -4
- package/static/dist/assets/{diagram-S7CK7UJ4-itodc50c.js → diagram-S7CK7UJ4-pa3ij6q7.js} +3 -3
- package/static/dist/assets/{diagram-UQ7AKVKN-n1h2t4ah.js → diagram-UQ7AKVKN-zbipyz57.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-lvtvm9bj.js → diagram-VSXAHHWV-d1kbpzgf.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-cgas58sy.js → diagram-VX7I27RA-i3ty14xl.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-d0aeps1q.js → diagram-Z3DM3KII-m8xefqa7.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-md345fyq.js → ebnfDiagram-PWID7BFC-nz7wj05l.js} +1 -1
- package/static/dist/assets/endpoints-k7yf25m0.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-i4rua63b.js → erDiagram-RLTQ6QDP-fa41ib83.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-kpbq09bx.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-lcz0kx14.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-geqx7zgl.js → ganttDiagram-EL5Y4UJY-n7zq01qu.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-leoq0z8t.js → gitGraph-4MIJSDKK-c89emb07.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-llrlxfha.js → gitGraphDiagram-WWUBYQGX-cz7cgr6w.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-kq2cnw6q.js → highlighted-body-KPVGNVTW-d9bapmni.js} +1 -1
- package/static/dist/assets/{index-f9bzglkn.js → index-h00usru2.js} +3 -3
- package/static/dist/assets/index-n73ijkv2.css +2 -0
- package/static/dist/assets/{info-A6RAGUB7-lkly4xj6.js → info-A6RAGUB7-ndddhhhh.js} +1 -1
- package/static/dist/assets/infoDiagram-27XIBGKW-ccxigcpf.js +2 -0
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-hq6jf374.js → ishikawaDiagram-5VMMS53U-le0z9vtc.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-is03ntb0.js → journeyDiagram-3NMN7TZE-ivsmb26o.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-jmerizqb.js → kanban-definition-UXKFOSKX-h6n2awt0.js} +1 -1
- package/static/dist/assets/lazyRouteComponent-m1rtyk4t.js +1 -0
- package/static/dist/assets/{line-laaa8h99.js → line-dmzlfe0u.js} +1 -1
- package/static/dist/assets/link-budeg5t9.js +1 -0
- package/static/dist/assets/login-kjhasvp8.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-jb5d211j.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-ix4junux.js → mermaid-parser.core-gwyd5bnq.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-o0e0adl1.js → mindmap-definition-YA3MSWOX-l68gg5h7.js} +1 -1
- package/static/dist/assets/onboarding-cdfrk0pj.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-kl8pwsyg.js → packet-AYTQ26CC-gpv06065.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-bbqn9jo4.js → pegDiagram-XKGWAZYB-e52vs23u.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-cpyo8bkr.js → pie-WAS4IAKB-l6m7nz5s.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-pelug34f.js → pieDiagram-E7YTZNPT-b4is36bq.js} +3 -3
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-jds4jmr5.js → quadrantDiagram-AXDQQJYC-m8vt2788.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-e02oxnvd.js → radar-RG4KPBEZ-nxbrhiov.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-kz3delwo.js → railroad-74A4TZTK-222mu9hi.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-kdvgl6x8.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-itiqslby.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-go7kov78.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-jgroj2ew.js → railroadDiagram-O6MQD6OU-o8lldfzv.js} +1 -1
- package/static/dist/assets/{refresh-cw-k7yxjdn3.js → refresh-cw-kbcje1uy.js} +1 -1
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-m4az5e0j.js → requirementDiagram-BXWQKSXE-ovyq38kv.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-vlryqiv3.js → sankeyDiagram-P5KCCOFB-ou7lznor.js} +1 -1
- package/static/dist/assets/{saved-brdfjjnw.js → saved-bazmcup9.js} +1 -1
- package/static/dist/assets/search-jen74z4u.js +1 -0
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-fr8hrpql.js → sequenceDiagram-WJ2MYXX4-ovckdzf2.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-l0zodj0b.js → settings_section_extensions_title-ertwwhlx.js} +1 -1
- package/static/dist/assets/share._token-dan9hgvj.js +2 -0
- package/static/dist/assets/{sliders-horizontal-gcgscv8r.js → sliders-horizontal-f9cb3lh3.js} +1 -1
- package/static/dist/assets/{stateDiagram-D77RDMKH-bg548iyr.js → stateDiagram-D77RDMKH-fcrexkkv.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-lcfraua2.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-hk5z3efh.js → swimlanes-42K2YHIH-f7usu8fw.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ogmanni0.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-olvknr62.js → timeline-definition-24CTP7MA-gp1hvz9f.js} +1 -1
- package/static/dist/assets/todoStore-eqtid4r4.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-ovvjsxtf.js → treeView-Q6P3EWNA-gb9gf11u.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-ggkipj59.js → treemap-WGGIJYW6-nuvfnvvi.js} +1 -1
- package/static/dist/assets/{untitled-c8ydw0bu.js → untitled-htelhc68.js} +1 -1
- package/static/dist/assets/useForm-h2t0u14e.js +1 -0
- package/static/dist/assets/useLocale-hcsb84pj.js +1 -0
- package/static/dist/assets/useNavigate-nwy70ztj.js +1 -0
- package/static/dist/assets/{useParams-katwvzil.js → useParams-dqd64jv7.js} +1 -1
- package/static/dist/assets/{useSearch-c70sgau1.js → useSearch-j30pvsy7.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-hzld7zt7.js → vennDiagram-4TSXK5OY-fzchbzaj.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-hxkeoms8.js → wardley-WFR3VGLG-m47mhfbh.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-ljh90mb1.js → wardleyDiagram-VM6X3IG4-odg6ct1z.js} +2 -2
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-mnuaes03.js → xychartDiagram-S5SC5T6Z-mrhms5um.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +1 -1
- package/static/dist/assets/AppShell-haqi9qzu.js +0 -1
- package/static/dist/assets/ChatPage-hjyu2g64.js +0 -22
- package/static/dist/assets/HubRoute-gkd8o1vg.js +0 -2
- package/static/dist/assets/RightPanel-o73y98dn.js +0 -1
- package/static/dist/assets/Select-jld3yeni.js +0 -1
- package/static/dist/assets/SessionListPanel-d3qx4ric.js +0 -1
- package/static/dist/assets/SettingsLayout-lnn2wo2s.js +0 -1
- package/static/dist/assets/States-e5hv6gdh.js +0 -1
- package/static/dist/assets/Toaster-c4jq1i26.js +0 -1
- package/static/dist/assets/_app-bnip2pjg.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-egptzrfy.js +0 -1
- package/static/dist/assets/_app.index-vdz13ul5.js +0 -1
- package/static/dist/assets/_app.insights-lgjshsba.js +0 -1
- package/static/dist/assets/_app.kanban-c18cjrpz.js +0 -1
- package/static/dist/assets/_app.logs-ezhojgeg.js +0 -1
- package/static/dist/assets/_app.memory-i00paisv.js +0 -1
- package/static/dist/assets/_app.profiles-kd02bwjx.js +0 -1
- package/static/dist/assets/_app.settings-giaudkuk.js +0 -1
- package/static/dist/assets/_app.settings._section-k4qtlexk.js +0 -1
- package/static/dist/assets/_app.skills-bby2uvl8.js +0 -1
- package/static/dist/assets/_app.tasks-1kz9bosm.js +0 -2
- package/static/dist/assets/_app.todos-izjdbkm2.js +0 -1
- package/static/dist/assets/_app.workspaces-lfh2kw7s.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-b4u9z1w3.js +0 -1
- package/static/dist/assets/architectureDiagram-5GKGNRK7-hqnf84fc.js +0 -36
- package/static/dist/assets/channel-o77ioktg.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-f5rvod26.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-f5rvod26.js +0 -1
- package/static/dist/assets/endpoints-e58saih3.js +0 -9
- package/static/dist/assets/eventmodeling-NTZA5JFV-bn3fjsaq.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-li70d120.js +0 -1
- package/static/dist/assets/index-gkh7kum4.css +0 -2
- package/static/dist/assets/infoDiagram-27XIBGKW-oo7kdyte.js +0 -2
- package/static/dist/assets/lazyRouteComponent-b1h2ykmb.js +0 -1
- package/static/dist/assets/link-d8f0788e.js +0 -1
- package/static/dist/assets/login-llkohrcg.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-id0ffk2r.js +0 -1
- package/static/dist/assets/onboarding-lx0d6b38.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-l13ped2z.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-c7rg3g4g.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-od0b8niq.js +0 -1
- package/static/dist/assets/search-gt2ueyvg.js +0 -1
- package/static/dist/assets/share._token-oyjv046r.js +0 -2
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-j5v40qih.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-grzefo9c.js +0 -8
- package/static/dist/assets/tabBuild-idliyw9f.js +0 -56
- package/static/dist/assets/todoStore-j4c2xq0b.js +0 -1
- package/static/dist/assets/useForm-kd759sjk.js +0 -1
- package/static/dist/assets/useLocale-hds4xmja.js +0 -1
package/dist/sessions/turn.js
CHANGED
|
@@ -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,14 +24,22 @@ 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,
|
|
28
|
-
import {
|
|
27
|
+
import { agentSteerText, assistantReplyAddedAfterCurrentTurn, buildPartialMessage, escapeWorkspacePrefixPath, checkpointTurnStart, extractToolCallsFromMessages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, sessionLacksFinalAssistantAnswer, splitDisplayText, joinReasoning, reasoningFieldsText, stoppedTurnContext, toolOutcome, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, 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';
|
|
30
|
+
import { WORKSPACE_BUSY_MESSAGE } from '../workspace/git.js';
|
|
29
31
|
import { str } from '../util.js';
|
|
30
32
|
export const CHAT_LOCK_WAIT_SECONDS = 2;
|
|
31
33
|
const IMAGE_MODE_TIMEOUT_MS = 15_000;
|
|
32
34
|
const TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT = 80;
|
|
35
|
+
/** How long a user's message waits for a stopped background turn to unwind before its own turn is admitted. */
|
|
36
|
+
const BACKGROUND_UNWIND_WAIT_MS = 30_000;
|
|
37
|
+
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.' };
|
|
38
|
+
/** `compressionExhausted`: the Agent flagged the turn itself, so its wording does not matter. */
|
|
33
39
|
export function classifyProviderError(errStr, opts = {}) {
|
|
34
40
|
const lower = errStr.toLowerCase();
|
|
41
|
+
if (opts.compressionExhausted)
|
|
42
|
+
return COMPRESSION_EXHAUSTED;
|
|
35
43
|
if (opts.condition === 'credential_missing')
|
|
36
44
|
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.' };
|
|
37
45
|
const cancelled = ['cancelled by user', 'canceled by user', 'user cancelled', 'user canceled', 'task cancelled', 'task canceled', 'cancellederror'].some((k) => lower.includes(k));
|
|
@@ -57,7 +65,7 @@ export function classifyProviderError(errStr, opts = {}) {
|
|
|
57
65
|
if (notFound)
|
|
58
66
|
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.' };
|
|
59
67
|
if (compressionExhausted)
|
|
60
|
-
return
|
|
68
|
+
return COMPRESSION_EXHAUSTED;
|
|
61
69
|
if (opts.silentFailure)
|
|
62
70
|
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.' };
|
|
63
71
|
return { label: 'Error', type: 'error', hint: '' };
|
|
@@ -148,6 +156,11 @@ export class TurnRunner {
|
|
|
148
156
|
abortControllers = new Map();
|
|
149
157
|
steers = new Map();
|
|
150
158
|
consumedSteers = new Map();
|
|
159
|
+
/** Per stream: steer, withdraw and Send now run one at a time, so each sees the queue the Agent holds. */
|
|
160
|
+
steerChains = new Map();
|
|
161
|
+
/** Streams whose Agent steer queue a withdraw or Send now is rewriting right now, and the pending text seen meanwhile. */
|
|
162
|
+
steerRewrites = new Map();
|
|
163
|
+
deferredPendingSteer = new Map();
|
|
151
164
|
lastCompletedTool = new Map();
|
|
152
165
|
/** Streams whose run completed (`done` emitted) and only await title work; a late cancel is a no-op for these. */
|
|
153
166
|
settledStreams = new Set();
|
|
@@ -158,6 +171,8 @@ export class TurnRunner {
|
|
|
158
171
|
/** 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
172
|
interrupts = new Map();
|
|
160
173
|
stopContexts = new Map();
|
|
174
|
+
/** Each turn's worker, so a stopped background turn can be awaited before the user's turn takes the session. */
|
|
175
|
+
workers = new Map();
|
|
161
176
|
constructor(deps) {
|
|
162
177
|
this.deps = deps;
|
|
163
178
|
}
|
|
@@ -170,6 +185,8 @@ export class TurnRunner {
|
|
|
170
185
|
return { error: 'Web is updating. Retry after it restarts.', _status: 503 };
|
|
171
186
|
if (this.deps.profileDeleting?.(s.profile ?? null))
|
|
172
187
|
return { error: `Profile '${str(s.profile)}' is being deleted.`, _status: 409 };
|
|
188
|
+
if (this.deps.workspaceBusy?.(opts.workspace))
|
|
189
|
+
return { error: WORKSPACE_BUSY_MESSAGE, _status: 409 };
|
|
173
190
|
const locked = s.active_stream_id;
|
|
174
191
|
if (locked) {
|
|
175
192
|
if (this.registry.liveIds.has(locked) || this.registry.activeRuns.has(locked))
|
|
@@ -184,9 +201,10 @@ export class TurnRunner {
|
|
|
184
201
|
return { error: 'session already has an active stream', active_stream_id: blocking, _status: 409 };
|
|
185
202
|
}
|
|
186
203
|
const streamId = randomUUID().replace(/-/g, '');
|
|
187
|
-
const wasHiddenEmpty = !s.messages.length && !s.context_messages.length && !s.
|
|
204
|
+
const wasHiddenEmpty = !s.messages.length && !s.context_messages.length && !s.hasPendingPrompt;
|
|
188
205
|
const attachments = opts.attachments ?? [];
|
|
189
|
-
|
|
206
|
+
// A background wakeup stays one in a forked session too (TAL-460).
|
|
207
|
+
const source = opts.source !== 'process_wakeup' && str(s.source_tag).toLowerCase() === 'fork' ? 'fork' : (opts.source ?? 'webui');
|
|
190
208
|
s.workspace = opts.workspace;
|
|
191
209
|
s.model = opts.model;
|
|
192
210
|
s.model_provider = opts.modelProvider;
|
|
@@ -214,10 +232,11 @@ export class TurnRunner {
|
|
|
214
232
|
const channel = this.registry.create(streamId, s.session_id);
|
|
215
233
|
if (opts.goalRelated)
|
|
216
234
|
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
|
-
|
|
235
|
+
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) });
|
|
236
|
+
const worker = this.run(s.session_id, streamId, channel, opts).catch((error) => {
|
|
219
237
|
this.deps.log(`[webui] ERROR turn ${streamId} crashed\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
220
|
-
});
|
|
238
|
+
}).finally(() => { this.workers.delete(streamId); });
|
|
239
|
+
this.workers.set(streamId, worker);
|
|
221
240
|
const response = { stream_id: streamId, session_id: s.session_id, pending_started_at: s.pending_started_at, turn_id: streamId, title: s.title };
|
|
222
241
|
if (opts.normalizedModel && opts.model)
|
|
223
242
|
response.effective_model = opts.model;
|
|
@@ -246,7 +265,7 @@ export class TurnRunner {
|
|
|
246
265
|
const put = (event, data, meta = {}) => {
|
|
247
266
|
for (const steerEvent of this.takeSteerEventsBefore(streamId, event))
|
|
248
267
|
put(steerEvent[0], steerEvent[1]);
|
|
249
|
-
if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', '
|
|
268
|
+
if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn'].includes(event))
|
|
250
269
|
return;
|
|
251
270
|
// `cancel()` already wrote the terminal row and closed the stream: the worker's unwind adds no second one.
|
|
252
271
|
if (event === 'cancel' && !this.registry.streams.has(streamId)) {
|
|
@@ -290,11 +309,7 @@ export class TurnRunner {
|
|
|
290
309
|
}
|
|
291
310
|
const msgText = opts.msg;
|
|
292
311
|
const previousMessages = structuredClone(s.messages);
|
|
293
|
-
|
|
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));
|
|
312
|
+
const previousContext = structuredClone(deps.service().modelContext(s));
|
|
298
313
|
// Python `_sanitize_messages_for_api`: the model never sees display-only rows or a replayed cancelled prompt.
|
|
299
314
|
const apiHistory = sanitizeMessagesForApi(previousContext);
|
|
300
315
|
const workspaceCtx = workspaceContextPrefix(opts.workspace);
|
|
@@ -314,6 +329,11 @@ export class TurnRunner {
|
|
|
314
329
|
const toolStartedAt = new WeakMap();
|
|
315
330
|
let tokenSent = false;
|
|
316
331
|
let firstTokenAt = null;
|
|
332
|
+
// TAL-460: a background turn's text is held back while it could still be a silence marker, so one never flashes by.
|
|
333
|
+
// ponytail: checks the turn's whole text, so a marker after earlier prose streams until the settled row hides it;
|
|
334
|
+
// check the last text segment instead if that shows up.
|
|
335
|
+
const holdSilence = turnOrigin(opts.source) === 'background';
|
|
336
|
+
let heldText = '';
|
|
317
337
|
const titles = new ReasoningTitleTracker();
|
|
318
338
|
let capturedTerminalError = null;
|
|
319
339
|
const controller = new AbortController();
|
|
@@ -327,6 +347,8 @@ export class TurnRunner {
|
|
|
327
347
|
activeRun.phase = 'running';
|
|
328
348
|
const settledAt = { value: false };
|
|
329
349
|
let failed = false;
|
|
350
|
+
// TAL-424: steers an errored turn ended without taking, sent as one follow-up turn once this one is torn down.
|
|
351
|
+
let followUp = [];
|
|
330
352
|
try {
|
|
331
353
|
if (!sidecar)
|
|
332
354
|
throw new SidecarError('The Agent sidecar is not running; chat is unavailable until it starts.', { condition: 'sidecar_unavailable' });
|
|
@@ -349,7 +371,7 @@ export class TurnRunner {
|
|
|
349
371
|
const frozenWorkspace = str(s.created_workspace) || str(s.workspace);
|
|
350
372
|
const turnContext = {
|
|
351
373
|
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' }),
|
|
374
|
+
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
375
|
max_iterations: processWakeupMaxIterations(maxIterationsFromConfig(cfg), opts.source ?? 'webui', deps.env ?? {}),
|
|
354
376
|
max_tokens: maxTokensFromConfig(cfg),
|
|
355
377
|
reasoning_config: reasoningConfigFromConfig(cfg, opts.modelProvider ?? s.model_provider ?? null),
|
|
@@ -375,7 +397,12 @@ export class TurnRunner {
|
|
|
375
397
|
if (stable !== null)
|
|
376
398
|
put('reasoning', { text: '', titles: stable });
|
|
377
399
|
partialText.push(str(data.text));
|
|
378
|
-
|
|
400
|
+
if (holdSilence && mayBecomeSilentReply(partialText.join(''))) {
|
|
401
|
+
heldText += str(data.text);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
put('token', { text: heldText + str(data.text) });
|
|
405
|
+
heldText = '';
|
|
379
406
|
return;
|
|
380
407
|
}
|
|
381
408
|
case 'reasoning': {
|
|
@@ -387,9 +414,12 @@ export class TurnRunner {
|
|
|
387
414
|
return;
|
|
388
415
|
}
|
|
389
416
|
case 'steer_pending':
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
417
|
+
// TAL-424: while a withdraw or Send now rewrites the Agent's queue its pending text is ambiguous; it settles after.
|
|
418
|
+
if (this.steerRewrites.has(streamId)) {
|
|
419
|
+
this.deferredPendingSteer.set(streamId, str(data.text));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.settleConsumedSteers(sessionId, streamId, str(data.text));
|
|
393
423
|
return;
|
|
394
424
|
case 'tool': {
|
|
395
425
|
const call = { name: data.name, args: data.args ?? {}, tid: str(data.tid), done: false };
|
|
@@ -401,15 +431,20 @@ export class TurnRunner {
|
|
|
401
431
|
return;
|
|
402
432
|
}
|
|
403
433
|
case 'tool_complete': {
|
|
404
|
-
// TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server
|
|
434
|
+
// TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
|
|
435
|
+
// are its display sections, which keep the stderr and exit code the flat preview drops.
|
|
405
436
|
const { raw_result: rawResult, ...complete } = data;
|
|
406
|
-
|
|
437
|
+
const outcome = toolOutcome(rawResult);
|
|
438
|
+
complete.is_error = outcome.is_error;
|
|
439
|
+
if (rawResult !== undefined)
|
|
440
|
+
complete.result_view = outcome.result_view;
|
|
407
441
|
const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
|
|
408
442
|
if (tc) {
|
|
409
443
|
const startedAt = toolStartedAt.get(tc);
|
|
410
444
|
if (startedAt !== undefined)
|
|
411
445
|
complete.duration = Math.round(Math.max(0, deps.now() - startedAt) * 1000) / 1000;
|
|
412
|
-
|
|
446
|
+
// TAL-315: the decided view too, so a failed or cancelled turn's snapshot keeps the sections shown live.
|
|
447
|
+
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 } : {}) });
|
|
413
448
|
}
|
|
414
449
|
const id = (tc && toolIds.get(tc)) || str(data.tid) || mintToolId();
|
|
415
450
|
this.lastCompletedTool.set(streamId, id);
|
|
@@ -424,6 +459,14 @@ export class TurnRunner {
|
|
|
424
459
|
put('approval', { ...(pending ?? data), pending_count });
|
|
425
460
|
return;
|
|
426
461
|
}
|
|
462
|
+
// TAL-514: the Agent stopped waiting on an approval (answer, timeout, or interrupt); withdraw its copy.
|
|
463
|
+
case 'approval_resolved': {
|
|
464
|
+
if (!str(data.approval_id))
|
|
465
|
+
return;
|
|
466
|
+
deps.pending.resolveApproval(sessionId, str(data.approval_id));
|
|
467
|
+
put(...this.approvalHeadFrame(sessionId));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
427
470
|
case 'clarify': {
|
|
428
471
|
deps.pending.submitClarify(sessionId, { ...data, session_id: sessionId, kind: 'clarify' });
|
|
429
472
|
const head = deps.pending.clarifyHeadFrame(sessionId);
|
|
@@ -455,25 +498,6 @@ export class TurnRunner {
|
|
|
455
498
|
put('cancel', this.cancelFrame(sessionId));
|
|
456
499
|
return;
|
|
457
500
|
}
|
|
458
|
-
if (opts.ephemeral) {
|
|
459
|
-
let answer = '';
|
|
460
|
-
for (let i = result.messages.length - 1; i >= 0; i -= 1) {
|
|
461
|
-
const m = result.messages[i];
|
|
462
|
-
if (m.role === 'assistant') {
|
|
463
|
-
answer = str(m.content);
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
opts.onDone?.(answer);
|
|
468
|
-
// Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
|
|
469
|
-
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' });
|
|
470
|
-
try {
|
|
471
|
-
rmSync(deps.store.pathFor(sessionId), { force: true });
|
|
472
|
-
}
|
|
473
|
-
catch { /* ignore */ }
|
|
474
|
-
deps.store.sessions.delete(sessionId);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
501
|
// Writeback only while this stream still owns the session (Python `_stream_writeback_is_current`).
|
|
478
502
|
let current;
|
|
479
503
|
try {
|
|
@@ -494,19 +518,26 @@ export class TurnRunner {
|
|
|
494
518
|
// sidecar reports `completed` whenever a failed run still carries messages).
|
|
495
519
|
// Python's second chance: a turn that emitted no new row still counts when the merged transcript it produced
|
|
496
520
|
// 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 });
|
|
498
|
-
|
|
521
|
+
const mergedForCheck = () => mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] });
|
|
522
|
+
// TAL-512: a btw answer must be a reply this turn added: everything before it is the parent's replayed history.
|
|
523
|
+
const assistantAdded = assistantReplyAddedAfterCurrentTurn(resultMessages, previousContext, msgText) || (!opts.ephemeral && !sessionLacksFinalAssistantAnswer(mergedForCheck()));
|
|
499
524
|
const lastErr = result.error ?? capturedTerminalError ?? '';
|
|
500
|
-
// Python `
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
525
|
+
// Python `_agent_result_terminal_failure`: the Agent's failed, partial, or compression-exhausted result ends the turn
|
|
526
|
+
// even after text streamed; only a partial with no error text and a final answer still completes.
|
|
527
|
+
const agentFailed = result.failed || result.compression_exhausted || (result.partial && (Boolean(lastErr) || !assistantAdded));
|
|
528
|
+
if (result.status === 'error' || agentFailed || (!assistantAdded && (!tokenSent || opts.ephemeral))) {
|
|
529
|
+
const classification = classifyProviderError(lastErr, { silentFailure: !lastErr, compressionExhausted: result.compression_exhausted });
|
|
504
530
|
const errStr = lastErr || `${classification.label}.`;
|
|
505
531
|
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
|
|
507
|
-
|
|
532
|
+
// Settle the steers first so the persisted turn carries every consumed one; the Agent's pending text is sent next.
|
|
533
|
+
// A withdraw or Send now still in flight finishes first, so the settlement sees the queue it left.
|
|
534
|
+
await this.steerRewrites.get(streamId);
|
|
535
|
+
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, str(result.pending_steer), 'followup');
|
|
536
|
+
followUp = leftovers;
|
|
508
537
|
this.persistError(s, streamId, classification.label, payload, activeTurnToken);
|
|
509
|
-
|
|
538
|
+
// TAL-512: a btw error carries no session: its journaled frame must not keep a copy of the parent conversation.
|
|
539
|
+
if (!opts.ephemeral)
|
|
540
|
+
payload.session = redactSessionData(this.terminalSessionPayload(s), deps.redactEnabled());
|
|
510
541
|
payload.session_id = s.session_id;
|
|
511
542
|
payload.old_session_id = sessionId;
|
|
512
543
|
for (const [event, data] of steerEvents)
|
|
@@ -515,17 +546,32 @@ export class TurnRunner {
|
|
|
515
546
|
failed = true;
|
|
516
547
|
return;
|
|
517
548
|
}
|
|
549
|
+
// TAL-512: a btw turn passes the failure check above first, so its last assistant row is this turn's reply.
|
|
550
|
+
if (opts.ephemeral) {
|
|
551
|
+
let answer = '';
|
|
552
|
+
for (let i = result.messages.length - 1; i >= 0; i -= 1) {
|
|
553
|
+
const m = result.messages[i];
|
|
554
|
+
if (m.role === 'assistant') {
|
|
555
|
+
answer = str(m.content);
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
opts.onDone?.(answer);
|
|
560
|
+
// Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
|
|
561
|
+
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' });
|
|
562
|
+
try {
|
|
563
|
+
rmSync(deps.store.pathFor(sessionId), { force: true });
|
|
564
|
+
}
|
|
565
|
+
catch { /* ignore */ }
|
|
566
|
+
deps.store.sessions.delete(sessionId);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
518
569
|
// ── settle the transcript ──
|
|
519
570
|
// The Agent's last pending-steer text settles the remaining steers before the turn is written back.
|
|
520
|
-
|
|
521
|
-
|
|
571
|
+
await this.steerRewrites.get(streamId);
|
|
572
|
+
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, result.pending_steer, 'followup');
|
|
573
|
+
s.messages = mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] });
|
|
522
574
|
s.context_messages = dedupeContext(resultMessages);
|
|
523
|
-
for (const m of s.messages) {
|
|
524
|
-
if (m.role !== 'assistant')
|
|
525
|
-
continue;
|
|
526
|
-
if (typeof m.content === 'string')
|
|
527
|
-
m.content = stripXmlToolCalls(m.content);
|
|
528
|
-
}
|
|
529
575
|
if (result.compressed) {
|
|
530
576
|
s.compression_anchor_visible_idx = Math.max(0, previousMessages.length - 1);
|
|
531
577
|
put('compressed', { session_id: sessionId, old_session_id: sessionId, new_session_id: sessionId, continuation_session_id: sessionId, message: 'Compression finished' });
|
|
@@ -567,7 +613,8 @@ export class TurnRunner {
|
|
|
567
613
|
if (m.role === 'user') {
|
|
568
614
|
const content = messageText(m.content);
|
|
569
615
|
const base = msgText.includes('\n\n[Attached files:') ? (msgText.split('\n\n[Attached files:')[0] ?? '').trim() : msgText;
|
|
570
|
-
|
|
616
|
+
// An attachment-only prompt has no text to match: only this turn's own row takes its files.
|
|
617
|
+
if (msgText ? content.includes(base.slice(0, 60)) || msgText.includes(content.slice(0, 60)) : m._turn_id === streamId)
|
|
571
618
|
m.attachments = [...attachments];
|
|
572
619
|
break;
|
|
573
620
|
}
|
|
@@ -583,12 +630,12 @@ export class TurnRunner {
|
|
|
583
630
|
asstIdx += 1;
|
|
584
631
|
if (turnIdx < prevAssistants)
|
|
585
632
|
continue;
|
|
586
|
-
|
|
633
|
+
// The settled row keeps its own fields: only its prose and its inline thinking (into `reasoning`) are rewritten.
|
|
587
634
|
if (typeof m.content === 'string' && m.content) {
|
|
588
|
-
const [content,
|
|
635
|
+
const [content, inline] = splitDisplayText(m.content);
|
|
589
636
|
m.content = content;
|
|
590
|
-
if (
|
|
591
|
-
m.reasoning =
|
|
637
|
+
if (inline)
|
|
638
|
+
m.reasoning = joinReasoning([reasoningFieldsText({ reasoning: m.reasoning }), inline]);
|
|
592
639
|
}
|
|
593
640
|
if (turnIdx === prevAssistants && reasoning.trim() && !m.reasoning)
|
|
594
641
|
m.reasoning = reasoning.trim();
|
|
@@ -635,6 +682,10 @@ export class TurnRunner {
|
|
|
635
682
|
cache_hit_percent: cacheHit(cacheReadTokens, inputTokens), turn_cache_hit_percent: cacheHit(turnCacheRead, turnInput), duration_seconds: Math.round(duration * 1000) / 1000,
|
|
636
683
|
context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
637
684
|
};
|
|
685
|
+
const doneSession = redactSessionData(this.terminalSessionPayload(s), deps.redactEnabled());
|
|
686
|
+
// TAL-299: the ring's figures match the terminal session's (and a detail reload's).
|
|
687
|
+
for (const key of CONTEXT_USAGE_FIELDS)
|
|
688
|
+
doneUsage[key] = doneSession[key];
|
|
638
689
|
if (usage.completion_tokens && duration > 0)
|
|
639
690
|
doneUsage.tps = Math.round((usage.completion_tokens / duration) * 10) / 10;
|
|
640
691
|
if (firstTokenAt !== null)
|
|
@@ -643,7 +694,7 @@ export class TurnRunner {
|
|
|
643
694
|
if (usedModel)
|
|
644
695
|
doneUsage.used_model = usedModel;
|
|
645
696
|
const donePayload = {
|
|
646
|
-
session:
|
|
697
|
+
session: doneSession,
|
|
647
698
|
usage: doneUsage,
|
|
648
699
|
terminal_state: turnTerminalState(s.messages, streamId),
|
|
649
700
|
};
|
|
@@ -657,22 +708,15 @@ export class TurnRunner {
|
|
|
657
708
|
// worker before its daemon-thread title generation).
|
|
658
709
|
this.registry.activeRuns.delete(streamId);
|
|
659
710
|
this.settledStreams.add(streamId);
|
|
660
|
-
|
|
711
|
+
this.startSteerFollowUp(sessionId, leftovers);
|
|
712
|
+
// Title work and the goal judge run side by side; `stream_end` follows both, so their frames reach the stream.
|
|
713
|
+
const work = await Promise.allSettled([this.backgroundTitle(s, put), this.continueGoal(s, streamId, put)]);
|
|
714
|
+
put('stream_end', { session_id: sessionId });
|
|
715
|
+
for (const r of work)
|
|
716
|
+
if (r.status === 'rejected')
|
|
717
|
+
throw r.reason;
|
|
661
718
|
// Python: the last non-error assistant reply with content, else "(no answer produced)".
|
|
662
|
-
|
|
663
|
-
let answer = '';
|
|
664
|
-
for (let i = s.messages.length - 1; i >= 0; i -= 1) {
|
|
665
|
-
const m = s.messages[i];
|
|
666
|
-
if (m.role !== 'assistant' || m._error)
|
|
667
|
-
continue;
|
|
668
|
-
const text = messageText(m.content).trim();
|
|
669
|
-
if (text) {
|
|
670
|
-
answer = text;
|
|
671
|
-
break;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
opts.onDone(answer || '(no answer produced)');
|
|
675
|
-
}
|
|
719
|
+
opts.onDone?.(lastAnswer(s.messages) || '(no answer produced)');
|
|
676
720
|
}
|
|
677
721
|
catch (error) {
|
|
678
722
|
if (settledAt.value && !(error instanceof SidecarError)) {
|
|
@@ -704,7 +748,8 @@ export class TurnRunner {
|
|
|
704
748
|
for (const [event, data] of this.takeSteerEventsBefore(streamId, 'apperror'))
|
|
705
749
|
put(event, data);
|
|
706
750
|
this.persistError(current, streamId, classification.label, payload, activeTurnToken);
|
|
707
|
-
|
|
751
|
+
if (!opts.ephemeral)
|
|
752
|
+
payload.session = redactSessionData(this.terminalSessionPayload(current), deps.redactEnabled());
|
|
708
753
|
}
|
|
709
754
|
}
|
|
710
755
|
catch (persistError) {
|
|
@@ -720,21 +765,26 @@ export class TurnRunner {
|
|
|
720
765
|
// not race the error writeback above (a re-saved session would resurface in the sidebar).
|
|
721
766
|
if (failed)
|
|
722
767
|
opts.onFailed?.();
|
|
768
|
+
this.startSteerFollowUp(sessionId, followUp);
|
|
723
769
|
}
|
|
724
770
|
}
|
|
725
771
|
/**
|
|
726
772
|
* Python `_build_user_message`: image attachments are embedded as native `image_url` parts only when the Agent's
|
|
727
773
|
* 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.
|
|
774
|
+
* only after the bytes are read through an anchored descriptor and sniffed as a real image format. Every attached
|
|
775
|
+
* file is also named by path after the text (TAL-276), so an attachment-only turn still gives the model a request
|
|
776
|
+
* and its transcript row a distinct identity.
|
|
729
777
|
*/
|
|
730
778
|
async buildUserMessage(workspaceCtx, msgText, attachments, workspace, sessionId, s, opts, signal) {
|
|
731
779
|
const text = workspaceCtx + msgText;
|
|
780
|
+
const named = attachments.map((att) => str(att.path).trim()).filter(Boolean).map(escapeWorkspacePrefixPath);
|
|
781
|
+
const withFiles = () => (named.length ? `${text}\n\n[Attached files: ${named.join(', ')}]` : text);
|
|
732
782
|
const candidates = attachments.filter((att) => str(att.path).trim() && str(att.mime).trim().startsWith('image/'));
|
|
733
783
|
if (!candidates.length)
|
|
734
|
-
return
|
|
784
|
+
return withFiles();
|
|
735
785
|
const sidecar = this.deps.sidecar();
|
|
736
786
|
if (!sidecar)
|
|
737
|
-
return
|
|
787
|
+
return withFiles();
|
|
738
788
|
// A cancel that landed before this point is final: never start the lookup or wait on it.
|
|
739
789
|
if (signal.aborted)
|
|
740
790
|
return text;
|
|
@@ -750,15 +800,15 @@ export class TurnRunner {
|
|
|
750
800
|
if (mode.mode !== 'native') {
|
|
751
801
|
const cfg = (await this.deps.profileConfig?.(s.profile ?? null)) ?? {};
|
|
752
802
|
if (explicitTextSignal(cfg) || mode.supports_vision === false)
|
|
753
|
-
return
|
|
803
|
+
return withFiles();
|
|
754
804
|
}
|
|
755
805
|
}
|
|
756
806
|
catch (error) {
|
|
757
807
|
if (!signal.aborted)
|
|
758
808
|
this.deps.log(`[webui] image mode lookup failed for ${sessionId}: ${error.message}`);
|
|
759
|
-
return
|
|
809
|
+
return withFiles();
|
|
760
810
|
}
|
|
761
|
-
const parts = [
|
|
811
|
+
const parts = [];
|
|
762
812
|
let images = 0;
|
|
763
813
|
const roots = [workspace, this.deps.attachmentDir(sessionId)].map((r) => resolvePathLikePython(r));
|
|
764
814
|
for (const att of candidates) {
|
|
@@ -790,17 +840,20 @@ export class TurnRunner {
|
|
|
790
840
|
closeSync(fd);
|
|
791
841
|
}
|
|
792
842
|
}
|
|
793
|
-
return images ? parts :
|
|
843
|
+
return images ? [{ type: 'text', text: withFiles() }, ...parts] : withFiles();
|
|
794
844
|
}
|
|
795
845
|
/**
|
|
796
846
|
* Python `_session_payload_with_terminal_window`: the same visible-message window contract as `GET /api/session?
|
|
797
847
|
* msg_limit=` (renderable rows, limited payload shape, windowed tool calls, todo state) with the full count.
|
|
798
848
|
*/
|
|
799
849
|
terminalSessionPayload(s) {
|
|
800
|
-
const payload = withSessionWireFlags(s.compact(), this.registry.liveIds);
|
|
801
|
-
|
|
850
|
+
const payload = withSessionWireFlags(s.compact({ contextLengthFor: this.deps.service().deps.contextLengthFor, modelOptionFor: this.deps.service().deps.modelOptionFor }), this.registry.liveIds);
|
|
851
|
+
payload.assistant_name = this.deps.service().assistantName(s);
|
|
852
|
+
payload.workspace_name = this.deps.service().workspaceNames()(s);
|
|
853
|
+
const service = this.deps.service();
|
|
854
|
+
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 }));
|
|
802
855
|
const [window, offset] = messageWindowForDisplay(scened, TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT, null);
|
|
803
|
-
const limited = messagesForLimitedPayload(window);
|
|
856
|
+
const limited = messagesForLimitedPayload(withBodyExcerpts(withDisplayMedia(window, service.mediaProjector(s)), s.active_stream_id));
|
|
804
857
|
payload.messages = limited;
|
|
805
858
|
payload.message_count = s.messages.length;
|
|
806
859
|
payload._messages_offset = offset;
|
|
@@ -853,7 +906,7 @@ export class TurnRunner {
|
|
|
853
906
|
}
|
|
854
907
|
materializePendingUserTurn(s, activeTurnToken, turnId) {
|
|
855
908
|
const pendingText = str(s.pending_user_message);
|
|
856
|
-
if (!
|
|
909
|
+
if (!s.hasPendingPrompt)
|
|
857
910
|
return false;
|
|
858
911
|
const recoveredTs = typeof s.pending_started_at === 'number' && s.pending_started_at > 0 ? s.pending_started_at : this.deps.now();
|
|
859
912
|
const source = s.pending_user_source ?? 'webui';
|
|
@@ -932,6 +985,11 @@ export class TurnRunner {
|
|
|
932
985
|
this.deps.store.sessions.delete(current.session_id);
|
|
933
986
|
return true;
|
|
934
987
|
}
|
|
988
|
+
// TAL-460: a background turn stops quietly: its prompt and model context are kept, but no partial and no Stop row,
|
|
989
|
+
// and its first settlement is final.
|
|
990
|
+
const quiet = this.registry.activeRuns.get(streamId)?.origin === 'background';
|
|
991
|
+
if (quiet && current.active_stream_id === null)
|
|
992
|
+
return true;
|
|
935
993
|
const stop = this.stopContexts.get(streamId);
|
|
936
994
|
const canonical = stop !== undefined && checkpoint !== null && checkpointTurnStart(checkpoint, stop.msgText, stop.historyLength) !== null;
|
|
937
995
|
const settle = () => {
|
|
@@ -967,8 +1025,10 @@ export class TurnRunner {
|
|
|
967
1025
|
current.pending_attachments = [];
|
|
968
1026
|
current.pending_started_at = null;
|
|
969
1027
|
current.pending_user_source = null;
|
|
970
|
-
|
|
971
|
-
|
|
1028
|
+
if (!quiet) {
|
|
1029
|
+
this.appendPartialSnapshot(current, streamId);
|
|
1030
|
+
current.messages.push({ role: 'assistant', content: '', _error: true, _terminal_state: 'cancelled', timestamp: Math.trunc(this.deps.now()), _turn_id: streamId });
|
|
1031
|
+
}
|
|
972
1032
|
this.persistConsumedSteers(current, streamId, startedAt, this.deps.now());
|
|
973
1033
|
try {
|
|
974
1034
|
this.deps.store.save(current);
|
|
@@ -1077,83 +1137,83 @@ export class TurnRunner {
|
|
|
1077
1137
|
const sessionId = s.session_id;
|
|
1078
1138
|
const placeholder = str(s.title).trim();
|
|
1079
1139
|
const invalidExisting = looksInvalidGeneratedTitle(s.title);
|
|
1080
|
-
|
|
1140
|
+
// A claimed CLI session still carrying its default `<Source> Session` title is retitled like a new chat (TAL-256).
|
|
1141
|
+
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 });
|
|
1142
|
+
const eligible = ['Untitled', 'New Chat', ''].includes(placeholder) || placeholder === titleFrom(s.messages, '') || invalidExisting || defaultTitle(s);
|
|
1081
1143
|
const [userText, assistantText] = firstExchangeSnippets(s.messages);
|
|
1082
1144
|
// Python `_put_title_status`: empty reason/title/raw_preview keys are omitted.
|
|
1083
1145
|
const status = (status, reason = '', title = '', rawPreview = '') => {
|
|
1084
1146
|
put('title_status', { session_id: sessionId, status, ...(reason ? { reason } : {}), ...(title ? { title } : {}), ...(rawPreview ? { raw_preview: rawPreview } : {}) });
|
|
1085
1147
|
};
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1148
|
+
if (!eligible || (s.llm_title_generated && !invalidExisting) || !userText || !assistantText)
|
|
1149
|
+
return;
|
|
1150
|
+
if (s.manual_title) {
|
|
1151
|
+
status('skipped', 'manual_title', placeholder);
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
if (!this.deps.titleGenerationEnabled()) {
|
|
1155
|
+
status('skipped', 'title_generation_disabled', placeholder);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const generated = await this.llmTitle(s, userText, assistantText);
|
|
1159
|
+
const { status: llmStatus, rawPreview } = generated;
|
|
1160
|
+
let next = generated.title;
|
|
1161
|
+
let source = llmStatus;
|
|
1162
|
+
if (!next) {
|
|
1163
|
+
const fallback = fallbackTitleFromExchange(userText, assistantText);
|
|
1164
|
+
if (fallback && !isGenericFallbackTitle(fallback)) {
|
|
1165
|
+
next = fallback;
|
|
1166
|
+
source = 'fallback';
|
|
1092
1167
|
}
|
|
1093
|
-
|
|
1094
|
-
|
|
1168
|
+
}
|
|
1169
|
+
const fallbackReason = source === 'fallback' && llmStatus ? `local_summary:${llmStatus}` : 'local_summary';
|
|
1170
|
+
let current = s;
|
|
1171
|
+
try {
|
|
1172
|
+
current = this.deps.store.get(sessionId);
|
|
1173
|
+
}
|
|
1174
|
+
catch {
|
|
1175
|
+
current = s;
|
|
1176
|
+
}
|
|
1177
|
+
let effective = str(current.title).trim();
|
|
1178
|
+
let wrote = false;
|
|
1179
|
+
if (next) {
|
|
1180
|
+
const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title) || defaultTitle(current);
|
|
1181
|
+
if (current.manual_title || !stillAuto) {
|
|
1182
|
+
status('skipped', 'manual_title', effective);
|
|
1095
1183
|
return;
|
|
1096
1184
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
const fallbackReason = source === 'fallback' && llmStatus ? `local_summary:${llmStatus}` : 'local_summary';
|
|
1109
|
-
let current = s;
|
|
1110
|
-
try {
|
|
1111
|
-
current = this.deps.store.get(sessionId);
|
|
1112
|
-
}
|
|
1113
|
-
catch {
|
|
1114
|
-
current = s;
|
|
1115
|
-
}
|
|
1116
|
-
let effective = str(current.title).trim();
|
|
1117
|
-
let wrote = false;
|
|
1118
|
-
if (next) {
|
|
1119
|
-
const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title);
|
|
1120
|
-
if (current.manual_title || !stillAuto) {
|
|
1121
|
-
status('skipped', 'manual_title', effective);
|
|
1122
|
-
return;
|
|
1123
|
-
}
|
|
1124
|
-
if (next !== effective) {
|
|
1125
|
-
current.title = next;
|
|
1126
|
-
markSessionTitleGenerated(current);
|
|
1127
|
-
this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1128
|
-
// Python `sync_session_title` after generation: the state.db row follows when `sync_to_insights` is on.
|
|
1129
|
-
await this.deps.syncTitle?.(current);
|
|
1130
|
-
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
1131
|
-
effective = next;
|
|
1132
|
-
wrote = true;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
if (wrote) {
|
|
1136
|
-
status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
|
|
1137
|
-
put('title', { session_id: sessionId, title: effective });
|
|
1185
|
+
if (next !== effective) {
|
|
1186
|
+
current.title = next;
|
|
1187
|
+
markSessionTitleGenerated(current);
|
|
1188
|
+
this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1189
|
+
// Python `sync_session_title` after generation: the state.db row follows when `sync_to_insights` is on.
|
|
1190
|
+
await this.deps.syncTitle?.(current);
|
|
1191
|
+
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
1192
|
+
effective = next;
|
|
1193
|
+
wrote = true;
|
|
1138
1194
|
}
|
|
1139
|
-
else
|
|
1140
|
-
status('skipped', source || 'unchanged', effective, rawPreview);
|
|
1141
1195
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1196
|
+
if (wrote) {
|
|
1197
|
+
status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
|
|
1198
|
+
put('title', { session_id: sessionId, title: effective });
|
|
1144
1199
|
}
|
|
1200
|
+
else
|
|
1201
|
+
status('skipped', source || 'unchanged', effective, rawPreview);
|
|
1145
1202
|
}
|
|
1146
1203
|
// ── cancel / steer ───────────────────────────────────────────────────────
|
|
1147
|
-
/**
|
|
1204
|
+
/**
|
|
1205
|
+
* Python `cancel_stream`: persist the partial, mark cancelled, interrupt the Agent, release admission. `withdrawn` is the
|
|
1206
|
+
* `steer_withdrawn` (stopped) this Stop emitted, for the cancel response (TAL-426).
|
|
1207
|
+
*/
|
|
1148
1208
|
async cancel(streamId) {
|
|
1149
1209
|
const channel = this.registry.peek(streamId);
|
|
1150
1210
|
const run = this.registry.activeRuns.get(streamId);
|
|
1151
1211
|
if (!channel && !run)
|
|
1152
|
-
return false;
|
|
1212
|
+
return { cancelled: false, withdrawn: [] };
|
|
1153
1213
|
// Python popped the run before its title work: a cancel that lands after `done` (while the title prompts still run)
|
|
1154
1214
|
// is not a cancellation, so it neither journals a `cancel` frame nor marks the completed run interrupted.
|
|
1155
1215
|
if (!run && this.settledStreams.has(streamId))
|
|
1156
|
-
return false;
|
|
1216
|
+
return { cancelled: false, withdrawn: [] };
|
|
1157
1217
|
const sessionId = this.registry.ownerSessionId(streamId) ?? run?.session_id ?? null;
|
|
1158
1218
|
this.registry.cancelled.add(streamId);
|
|
1159
1219
|
if (run) {
|
|
@@ -1174,7 +1234,9 @@ export class TurnRunner {
|
|
|
1174
1234
|
const leftover = str(reply?.pending_steer);
|
|
1175
1235
|
const checkpoint = reply?.checkpoint ?? null;
|
|
1176
1236
|
// Settle the steers first so the persisted cancel carries every consumed one.
|
|
1177
|
-
|
|
1237
|
+
// TAL-424: a Stop withdraws the steers the Agent never took, with their text, and starts nothing after.
|
|
1238
|
+
await this.steerRewrites.get(streamId);
|
|
1239
|
+
const steerEvents = (this.steers.get(streamId) ?? []).length ? this.finalizeSteers(streamId, leftover, 'stopped').events : [];
|
|
1178
1240
|
if (sessionId) {
|
|
1179
1241
|
let current = null;
|
|
1180
1242
|
try {
|
|
@@ -1210,6 +1272,21 @@ export class TurnRunner {
|
|
|
1210
1272
|
this.registry.liveIds.delete(streamId);
|
|
1211
1273
|
}
|
|
1212
1274
|
this.abortControllers.get(streamId)?.abort();
|
|
1275
|
+
const withdrawn = steerEvents.filter(([event]) => event === 'steer_withdrawn').map(([, data]) => data);
|
|
1276
|
+
return { cancelled: true, withdrawn };
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* TAL-460: a user's message never joins a background turn. That turn stops quietly (`finalizeCancelled`) and its worker
|
|
1280
|
+
* unwinds before the user's turn is admitted, so two Agent runs never share the session. True when one was stopped.
|
|
1281
|
+
*/
|
|
1282
|
+
async yieldBackgroundTurn(sessionId) {
|
|
1283
|
+
const streamId = this.registry.activeRunStreamForSession(sessionId);
|
|
1284
|
+
if (!streamId || this.registry.activeRuns.get(streamId)?.origin !== 'background')
|
|
1285
|
+
return false;
|
|
1286
|
+
await this.cancel(streamId);
|
|
1287
|
+
let timer;
|
|
1288
|
+
await Promise.race([this.workers.get(streamId), new Promise((resolve) => { timer = setTimeout(resolve, BACKGROUND_UNWIND_WAIT_MS); })]);
|
|
1289
|
+
clearTimeout(timer);
|
|
1213
1290
|
return true;
|
|
1214
1291
|
}
|
|
1215
1292
|
async steer(sessionId, text, displayText, steerId) {
|
|
@@ -1234,7 +1311,10 @@ export class TurnRunner {
|
|
|
1234
1311
|
return { accepted: false, fallback: 'no_cached_agent', stream_id: null };
|
|
1235
1312
|
// Register provisionally before the RPC: the Agent may apply the steer and finish the turn before the steer
|
|
1236
1313
|
// reply arrives, and the turn's finalisation must then already see the record. A rejection removes it again.
|
|
1237
|
-
|
|
1314
|
+
return this.withSteerChain(activeStreamId, () => this.steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId));
|
|
1315
|
+
}
|
|
1316
|
+
async steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId) {
|
|
1317
|
+
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
1318
|
const records = this.steers.get(activeStreamId) ?? [];
|
|
1239
1319
|
records.push(record);
|
|
1240
1320
|
this.steers.set(activeStreamId, records);
|
|
@@ -1259,6 +1339,12 @@ export class TurnRunner {
|
|
|
1259
1339
|
// An accepted steer whose turn finalised while the reply was in flight was already reported by that finalisation.
|
|
1260
1340
|
if (!result.accepted)
|
|
1261
1341
|
withdraw();
|
|
1342
|
+
else {
|
|
1343
|
+
record.can_redirect = result.can_redirect === true;
|
|
1344
|
+
// TAL-424: every client shows it until the Agent takes it (unless it already did while the reply was in flight).
|
|
1345
|
+
if (this.steers.get(activeStreamId)?.includes(record))
|
|
1346
|
+
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
1347
|
+
}
|
|
1262
1348
|
return { accepted: result.accepted, fallback: result.accepted ? null : (result.fallback ?? 'not_running'), stream_id: activeStreamId, steer_id: steerId };
|
|
1263
1349
|
}
|
|
1264
1350
|
/** Python `_webui_steer_events_before` for terminal frames: cancel/apperror flush consumed steers and leftovers first. */
|
|
@@ -1267,7 +1353,7 @@ export class TurnRunner {
|
|
|
1267
1353
|
return [];
|
|
1268
1354
|
if (!(this.steers.get(streamId) ?? []).length)
|
|
1269
1355
|
return [];
|
|
1270
|
-
return this.
|
|
1356
|
+
return this.finalizeSteers(streamId, '', 'stopped').events;
|
|
1271
1357
|
}
|
|
1272
1358
|
/** Python `_pending_webui_steer_suffix_start`: the pending text is the newline-joined tail of the records; earlier ones are consumed. */
|
|
1273
1359
|
static pendingSteerSuffixStart(records, pendingText) {
|
|
@@ -1280,15 +1366,15 @@ export class TurnRunner {
|
|
|
1280
1366
|
}
|
|
1281
1367
|
return null;
|
|
1282
1368
|
}
|
|
1283
|
-
consumedSteerPayload(record) {
|
|
1369
|
+
consumedSteerPayload(record, opts = {}) {
|
|
1284
1370
|
const consumedAt = this.deps.now();
|
|
1285
1371
|
const afterToolCallId = this.lastCompletedTool.get(record.stream_id) ?? null;
|
|
1286
1372
|
const consumed = this.consumedSteers.get(record.stream_id) ?? [];
|
|
1287
1373
|
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 });
|
|
1374
|
+
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
1375
|
this.consumedSteers.set(record.stream_id, consumed);
|
|
1290
1376
|
}
|
|
1291
|
-
return {
|
|
1377
|
+
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
1378
|
}
|
|
1293
1379
|
/**
|
|
1294
1380
|
* Consumed steers become display-only `_steer` user rows at their causal place in the turn: after the tool results of
|
|
@@ -1309,7 +1395,14 @@ export class TurnRunner {
|
|
|
1309
1395
|
};
|
|
1310
1396
|
// An Agent that records the steer it delivered does so at its exact place: that row becomes the steer, not a copy.
|
|
1311
1397
|
const agentRows = s.messages.flatMap((m, i) => (m._turn_id === streamId && agentSteerText(m) !== null ? [i] : []));
|
|
1312
|
-
|
|
1398
|
+
let agentRow = agentRows.find((i) => agentSteerText(s.messages[i]) === steer.agent_text.trim()) ?? agentRows[0];
|
|
1399
|
+
// A Send now redirect is recorded as a plain user row of the turn: that row becomes the steer too.
|
|
1400
|
+
if (steer.redirected) {
|
|
1401
|
+
const prompt = s.messages.findIndex((m) => m._turn_id === streamId && m.role === 'user' && !m._steer);
|
|
1402
|
+
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());
|
|
1403
|
+
// Never another steer's row: without its own correction row (the Agent joins two redirects) it gets a new one.
|
|
1404
|
+
agentRow = own >= 0 ? own : undefined;
|
|
1405
|
+
}
|
|
1313
1406
|
if (agentRow === undefined)
|
|
1314
1407
|
s.messages.splice(steerInsertIndex(s.messages, streamId, steer.after_tool_call_id), 0, row);
|
|
1315
1408
|
else
|
|
@@ -1364,22 +1457,235 @@ export class TurnRunner {
|
|
|
1364
1457
|
return [];
|
|
1365
1458
|
return records.slice(0, start).map((r) => this.consumedSteerPayload(r));
|
|
1366
1459
|
}
|
|
1367
|
-
/**
|
|
1368
|
-
|
|
1460
|
+
/**
|
|
1461
|
+
* Terminal steer bookkeeping: the records the Agent no longer holds are consumed; the rest it never took are either
|
|
1462
|
+
* `stopped` (withdrawn with their text for the composer) or returned as one `followup` turn's texts (TAL-424).
|
|
1463
|
+
*/
|
|
1464
|
+
finalizeSteers(streamId, leftoverText, mode) {
|
|
1369
1465
|
const records = this.steers.get(streamId) ?? [];
|
|
1370
1466
|
this.steers.delete(streamId);
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
const
|
|
1375
|
-
const
|
|
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');
|
|
1467
|
+
// Text that ends with none of these steers is another surface's: the Agent took every one of them.
|
|
1468
|
+
const start = TurnRunner.pendingSteerSuffixStart(records, leftoverText) ?? records.length;
|
|
1469
|
+
const consumed = records.slice(0, start);
|
|
1470
|
+
const left = records.slice(start);
|
|
1471
|
+
const matched = left.map((r) => r.text).join('\n');
|
|
1379
1472
|
const unmatched = matched ? leftoverText.slice(0, leftoverText.length - matched.length).replace(/\n+$/, '') : leftoverText;
|
|
1473
|
+
const events = consumed.map((r) => ['steer_consumed', this.consumedSteerPayload(r)]);
|
|
1474
|
+
for (const r of left)
|
|
1475
|
+
events.push(['steer_withdrawn', { steer_id: r.steer_id, reason: mode, text: r.display_text }]);
|
|
1476
|
+
if (mode === 'followup')
|
|
1477
|
+
return { events, leftovers: [...(unmatched ? [unmatched] : []), ...left.map((r) => r.text)] };
|
|
1478
|
+
if (unmatched)
|
|
1479
|
+
events.push(['steer_withdrawn', { steer_id: null, reason: 'stopped', text: unmatched }]);
|
|
1480
|
+
// ponytail: legacy `pending_steer_leftover` keeps today's Web and App from losing a stopped steer (they requeue it);
|
|
1481
|
+
// drop it once TAL-425 / TAL-426 put `steer_withdrawn` text back in the composer.
|
|
1482
|
+
for (const r of left)
|
|
1483
|
+
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
1484
|
if (unmatched)
|
|
1381
1485
|
events.push(['pending_steer_leftover', { stream_id: streamId, text: unmatched }]);
|
|
1382
|
-
return events;
|
|
1486
|
+
return { events, leftovers: [] };
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* TAL-424: the steers a finished turn never took go on as one follow-up turn, sent once by the server for every client.
|
|
1490
|
+
* Should another turn already hold the session, they steer into that one instead, so no text is lost.
|
|
1491
|
+
*/
|
|
1492
|
+
startSteerFollowUp(sessionId, texts) {
|
|
1493
|
+
if (!texts.length)
|
|
1494
|
+
return;
|
|
1495
|
+
let s;
|
|
1496
|
+
try {
|
|
1497
|
+
s = this.deps.store.get(sessionId);
|
|
1498
|
+
}
|
|
1499
|
+
catch {
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const msg = texts.join('\n');
|
|
1503
|
+
const started = this.start(s, { msg, attachments: [], workspace: s.workspace, model: s.model, modelProvider: s.model_provider });
|
|
1504
|
+
if (!started._status || started._status < 400)
|
|
1505
|
+
return;
|
|
1506
|
+
if (started._status === 409 && started.active_stream_id) {
|
|
1507
|
+
void this.steer(sessionId, msg, msg, `steer-${randomUUID()}`).then((r) => {
|
|
1508
|
+
if (r.accepted !== true)
|
|
1509
|
+
this.deps.log(`[webui] WARNING: steer follow-up for ${sessionId} was not accepted (${str(r.fallback)}); text: ${msg.slice(0, 200)}`);
|
|
1510
|
+
});
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
this.deps.log(`[webui] WARNING: steer follow-up for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
|
|
1514
|
+
}
|
|
1515
|
+
/**
|
|
1516
|
+
* TAL-396 (Agent gateway `_post_turn_goal_continuation`): after every turn settles, the Agent's GoalManager judges it
|
|
1517
|
+
* while a goal is active and the server starts the continuation turn it asks for. Any turn counts, so a resumed or
|
|
1518
|
+
* persisted goal picks up from the next user message. A user turn that took the session meanwhile runs first and is
|
|
1519
|
+
* judged next. A stopped or failed turn never gets here, so the run ends there.
|
|
1520
|
+
*/
|
|
1521
|
+
async continueGoal(s, streamId, put) {
|
|
1522
|
+
const sidecar = this.deps.sidecar();
|
|
1523
|
+
const lastResponse = lastAnswer(s.messages);
|
|
1524
|
+
// The Agent never drives a goal from an empty reply.
|
|
1525
|
+
if (!sidecar || !lastResponse)
|
|
1526
|
+
return;
|
|
1527
|
+
const sessionId = s.session_id;
|
|
1528
|
+
// Only a known goal turn shows progress up front; any other turn shows nothing unless a goal turns out to be active.
|
|
1529
|
+
const goalTurn = this.registry.goalRelated.has(streamId);
|
|
1530
|
+
if (goalTurn)
|
|
1531
|
+
put('goal', { session_id: sessionId, state: 'evaluating', message: 'Evaluating goal progress…', message_key: 'goal_evaluating_progress' });
|
|
1532
|
+
let decision;
|
|
1533
|
+
try {
|
|
1534
|
+
decision = await sidecar.call('goals.evaluate', { session_id: sessionId, profile_home: this.deps.profileHome(s.profile), last_response: lastResponse, user_initiated: true });
|
|
1535
|
+
}
|
|
1536
|
+
catch (error) {
|
|
1537
|
+
this.deps.log(`[webui] WARNING: goal evaluation failed for ${sessionId}: ${error.message}`);
|
|
1538
|
+
if (goalTurn)
|
|
1539
|
+
put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
if (!goalTurn && decision.verdict === 'inactive')
|
|
1543
|
+
return;
|
|
1544
|
+
const prompt = str(decision.continuation_prompt).trim();
|
|
1545
|
+
const proceed = decision.should_continue && Boolean(prompt);
|
|
1546
|
+
const status = { message: decision.message, message_key: decision.message_key, message_args: decision.message_args ?? [], decision: decision.verdict };
|
|
1547
|
+
put('goal', { session_id: sessionId, state: proceed ? 'continuing' : 'idle', ...status });
|
|
1548
|
+
if (!proceed)
|
|
1549
|
+
return;
|
|
1550
|
+
let current;
|
|
1551
|
+
try {
|
|
1552
|
+
current = this.deps.store.get(sessionId);
|
|
1553
|
+
}
|
|
1554
|
+
catch {
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
const started = this.start(current, { msg: prompt, attachments: [], workspace: current.workspace, model: current.model, modelProvider: current.model_provider, goalRelated: true });
|
|
1558
|
+
if (started._status === 409 && started.active_stream_id) {
|
|
1559
|
+
this.registry.goalRelated.add(started.active_stream_id);
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
if (started._status !== undefined && started._status >= 400) {
|
|
1563
|
+
this.deps.log(`[webui] WARNING: goal continuation for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
|
|
1564
|
+
put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
put('goal_continue', { session_id: sessionId, state: 'continuing', continuation_prompt: prompt, text: prompt, stream_id: str(started.stream_id), ...status });
|
|
1568
|
+
}
|
|
1569
|
+
/** The Agent's pending steer text settles which steers it has taken: those become `steer_consumed` and their rows. */
|
|
1570
|
+
settleConsumedSteers(sessionId, streamId, pendingText) {
|
|
1571
|
+
for (const record of this.takeConsumedSteers(streamId, pendingText, { keepLeftovers: true }))
|
|
1572
|
+
this.emitToSession(sessionId, 'steer_consumed', record);
|
|
1573
|
+
this.saveConsumedSteers(sessionId, streamId);
|
|
1574
|
+
}
|
|
1575
|
+
/** What every client shows for a pending steer; the server decides which actions it offers. */
|
|
1576
|
+
pendingSteerView(r) {
|
|
1577
|
+
const open = r.state === 'pending';
|
|
1578
|
+
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 } };
|
|
1579
|
+
}
|
|
1580
|
+
/** TAL-424: the active stream's pending steers, oldest first (session detail `pending_steers`). */
|
|
1581
|
+
pendingSteers(streamId) {
|
|
1582
|
+
return (this.steers.get(streamId) ?? []).map((r) => this.pendingSteerView(r));
|
|
1583
|
+
}
|
|
1584
|
+
withSteerChain(streamId, fn) {
|
|
1585
|
+
const run = (this.steerChains.get(streamId) ?? Promise.resolve()).then(fn);
|
|
1586
|
+
const settled = run.catch(() => undefined).then(() => { if (this.steerChains.get(streamId) === settled)
|
|
1587
|
+
this.steerChains.delete(streamId); });
|
|
1588
|
+
this.steerChains.set(streamId, settled);
|
|
1589
|
+
return run;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* A withdraw or Send now on one pending steer of the session's active stream. `rewrite` asks the sidecar to change the
|
|
1593
|
+
* Agent's queue and says whether it did; pending text the Agent reported meanwhile is settled after, unless the queue
|
|
1594
|
+
* changed (the next report then reflects it).
|
|
1595
|
+
*/
|
|
1596
|
+
async rewriteSteer(sessionId, steerId, fallback, rewrite) {
|
|
1597
|
+
let s = null;
|
|
1598
|
+
try {
|
|
1599
|
+
s = this.deps.store.get(sessionId, { metadataOnly: true });
|
|
1600
|
+
}
|
|
1601
|
+
catch {
|
|
1602
|
+
s = null;
|
|
1603
|
+
}
|
|
1604
|
+
const streamId = s?.active_stream_id;
|
|
1605
|
+
const sidecar = this.deps.sidecar();
|
|
1606
|
+
if (!streamId || !sidecar)
|
|
1607
|
+
return fallback;
|
|
1608
|
+
return this.withSteerChain(streamId, async () => {
|
|
1609
|
+
const records = this.steers.get(streamId) ?? [];
|
|
1610
|
+
const index = records.findIndex((r) => r.steer_id === steerId);
|
|
1611
|
+
if (index < 0 || records[index].state !== 'pending')
|
|
1612
|
+
return fallback;
|
|
1613
|
+
// In flight before the sidecar call, so a pending-text report racing its reply is deferred too.
|
|
1614
|
+
let finished = () => undefined;
|
|
1615
|
+
this.steerRewrites.set(streamId, new Promise((resolve) => { finished = resolve; }));
|
|
1616
|
+
let outcome = { result: fallback, changed: false };
|
|
1617
|
+
try {
|
|
1618
|
+
outcome = await rewrite(sidecar, streamId, records, index);
|
|
1619
|
+
}
|
|
1620
|
+
catch {
|
|
1621
|
+
outcome = { result: fallback, changed: false };
|
|
1622
|
+
}
|
|
1623
|
+
finally {
|
|
1624
|
+
this.steerRewrites.delete(streamId);
|
|
1625
|
+
finished();
|
|
1626
|
+
const deferred = this.deferredPendingSteer.get(streamId);
|
|
1627
|
+
this.deferredPendingSteer.delete(streamId);
|
|
1628
|
+
if (deferred !== undefined && !outcome.changed)
|
|
1629
|
+
this.settleConsumedSteers(sessionId, streamId, deferred);
|
|
1630
|
+
}
|
|
1631
|
+
return outcome.result;
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
dropSteer(streamId, record) {
|
|
1635
|
+
const records = this.steers.get(streamId);
|
|
1636
|
+
const index = records?.indexOf(record) ?? -1;
|
|
1637
|
+
if (records && index >= 0)
|
|
1638
|
+
records.splice(index, 1);
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* TAL-424 Edit and Cancel: take a pending steer back before the Agent takes it. `withdrawn: false` (and nothing
|
|
1642
|
+
* emitted) when it is unknown, already taken or being sent; a taken one then settles as consumed.
|
|
1643
|
+
*/
|
|
1644
|
+
withdrawSteer(sessionId, steerId, reason) {
|
|
1645
|
+
return this.rewriteSteer(sessionId, steerId, { withdrawn: false }, async (sidecar, streamId, records, index) => {
|
|
1646
|
+
const record = records[index];
|
|
1647
|
+
const reply = await sidecar.call('chat.steer_withdraw', { stream_id: streamId, pending: records.map((r) => r.text), index });
|
|
1648
|
+
if (!reply.withdrawn)
|
|
1649
|
+
return { result: { withdrawn: false }, changed: false };
|
|
1650
|
+
this.dropSteer(streamId, record);
|
|
1651
|
+
this.emitToSession(sessionId, 'steer_withdrawn', { steer_id: record.steer_id, reason, text: record.display_text });
|
|
1652
|
+
return { result: { withdrawn: true, text: record.display_text }, changed: true };
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* TAL-424 Send now: deliver a pending steer with the Agent's redirect. A model request restarts with it (consumed at
|
|
1657
|
+
* once); during tools it goes last on the Agent's queue and stays `sending_now` until taken; with nothing live it
|
|
1658
|
+
* stays pending.
|
|
1659
|
+
*/
|
|
1660
|
+
sendSteerNow(sessionId, steerId) {
|
|
1661
|
+
return this.rewriteSteer(sessionId, steerId, { redirected: false }, async (sidecar, streamId, records, index) => {
|
|
1662
|
+
const record = records[index];
|
|
1663
|
+
if (!record.can_redirect)
|
|
1664
|
+
return { result: { redirected: false }, changed: false };
|
|
1665
|
+
const reply = await sidecar.call('chat.steer_now', { stream_id: streamId, pending: records.map((r) => r.text), index });
|
|
1666
|
+
if (!reply.withdrawn)
|
|
1667
|
+
return { result: { redirected: false }, changed: false };
|
|
1668
|
+
if (!reply.redirected) {
|
|
1669
|
+
// The Agent's queue changed meanwhile, so it went back last: clients follow the new order.
|
|
1670
|
+
if (reply.requeued === 'last') {
|
|
1671
|
+
this.dropSteer(streamId, record);
|
|
1672
|
+
records.push(record);
|
|
1673
|
+
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
1674
|
+
}
|
|
1675
|
+
return { result: { redirected: false }, changed: true };
|
|
1676
|
+
}
|
|
1677
|
+
this.dropSteer(streamId, record);
|
|
1678
|
+
if (reply.delivery === 'steer') {
|
|
1679
|
+
record.state = 'sending_now';
|
|
1680
|
+
records.push(record);
|
|
1681
|
+
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
1682
|
+
}
|
|
1683
|
+
else {
|
|
1684
|
+
this.emitToSession(sessionId, 'steer_consumed', this.consumedSteerPayload(record, { redirected: true }));
|
|
1685
|
+
this.saveConsumedSteers(sessionId, streamId);
|
|
1686
|
+
}
|
|
1687
|
+
return { result: { redirected: true }, changed: true };
|
|
1688
|
+
});
|
|
1383
1689
|
}
|
|
1384
1690
|
// ── approvals / clarify ──────────────────────────────────────────────────
|
|
1385
1691
|
async respondApproval(sessionId, choice, approvalId, enableYolo) {
|
|
@@ -1402,6 +1708,7 @@ export class TurnRunner {
|
|
|
1402
1708
|
}
|
|
1403
1709
|
const profileHome = this.deps.profileHome(s?.profile ?? null);
|
|
1404
1710
|
let resolved = false;
|
|
1711
|
+
let withdrawn = false;
|
|
1405
1712
|
if (entry) {
|
|
1406
1713
|
if (!sidecar)
|
|
1407
1714
|
return relayFailure('not running');
|
|
@@ -1413,8 +1720,13 @@ export class TurnRunner {
|
|
|
1413
1720
|
this.deps.log(`[webui] approval relay failed for ${sessionId}: ${error.message}`);
|
|
1414
1721
|
return relayFailure(str(error.message));
|
|
1415
1722
|
}
|
|
1416
|
-
if (resolved
|
|
1723
|
+
if (resolved)
|
|
1417
1724
|
this.deps.pending.resolveApproval(sessionId, str(entry.approval_id));
|
|
1725
|
+
else {
|
|
1726
|
+
// TAL-514: a rejected answer can mean the Agent already stopped waiting on this card; drop what it no longer holds.
|
|
1727
|
+
await this.reconcileApprovals(sessionId, sidecar);
|
|
1728
|
+
withdrawn = !this.deps.pending.hasApprovalId(sessionId, str(entry.approval_id));
|
|
1729
|
+
}
|
|
1418
1730
|
}
|
|
1419
1731
|
let yoloEnabled;
|
|
1420
1732
|
if (enableYolo) {
|
|
@@ -1437,10 +1749,34 @@ export class TurnRunner {
|
|
|
1437
1749
|
yoloEnabled = true;
|
|
1438
1750
|
this.deps.pending.clearApprovals(sessionId);
|
|
1439
1751
|
}
|
|
1752
|
+
if (withdrawn)
|
|
1753
|
+
return { ok: true, choice, stale_cleared: true, pending_count: this.deps.pending.approvalPending(sessionId).pending_count, ...(enableYolo ? { yolo_enabled: yoloEnabled } : {}) };
|
|
1440
1754
|
if (!found && !this.deps.pending.hasPendingApproval(sessionId))
|
|
1441
1755
|
return { ok: true, choice, stale_cleared: true, ...(enableYolo ? { yolo_enabled: yoloEnabled } : {}) };
|
|
1442
1756
|
return { ok: resolved || !approvalId, choice, ...(enableYolo && (resolved || !approvalId) ? { yolo_enabled: yoloEnabled } : {}) };
|
|
1443
1757
|
}
|
|
1758
|
+
/** The live chat frame after approvals were withdrawn: the new queue head, or `approval_cleared` once none is left. */
|
|
1759
|
+
approvalHeadFrame(sessionId) {
|
|
1760
|
+
const { pending, pending_count } = this.deps.pending.approvalPending(sessionId);
|
|
1761
|
+
return pending ? ['approval', { ...pending, pending_count }] : ['approval_cleared', { session_id: sessionId, pending_count: 0 }];
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Drop mirrored approvals the Agent no longer holds; a failed read keeps them (the card stays answerable). Only
|
|
1765
|
+
* entries queued before the read are candidates: one the Agent parks meanwhile can be missing from its snapshot.
|
|
1766
|
+
*/
|
|
1767
|
+
async reconcileApprovals(sessionId, sidecar) {
|
|
1768
|
+
const before = this.deps.pending.approvalRequestIds(sessionId);
|
|
1769
|
+
let live;
|
|
1770
|
+
try {
|
|
1771
|
+
live = new Set((await sidecar.call('approval.pending', { session_id: sessionId })).pending.map((p) => str(p.request_id)));
|
|
1772
|
+
}
|
|
1773
|
+
catch (error) {
|
|
1774
|
+
this.deps.log(`[webui] approval reconcile failed for ${sessionId}: ${error.message}`);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (this.deps.pending.dropApprovals(sessionId, new Set(before.filter((id) => !live.has(id)))).length)
|
|
1778
|
+
this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId));
|
|
1779
|
+
}
|
|
1444
1780
|
withYoloLock(sessionId, fn) {
|
|
1445
1781
|
const run = (this.yoloChains.get(sessionId) ?? Promise.resolve()).then(fn);
|
|
1446
1782
|
const settled = run.catch(() => undefined).then(() => { if (this.yoloChains.get(sessionId) === settled)
|
|
@@ -1493,6 +1829,18 @@ export class TurnRunner {
|
|
|
1493
1829
|
return { ok: true, response };
|
|
1494
1830
|
}
|
|
1495
1831
|
}
|
|
1832
|
+
/** The turn's answer: the last non-error assistant reply with content. */
|
|
1833
|
+
function lastAnswer(messages) {
|
|
1834
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
1835
|
+
const m = messages[i];
|
|
1836
|
+
if (m.role !== 'assistant' || m._error)
|
|
1837
|
+
continue;
|
|
1838
|
+
const text = messageText(m.content).trim();
|
|
1839
|
+
if (text)
|
|
1840
|
+
return text;
|
|
1841
|
+
}
|
|
1842
|
+
return '';
|
|
1843
|
+
}
|
|
1496
1844
|
function previousStartedAt(s, run) {
|
|
1497
1845
|
return typeof s.pending_started_at === 'number' && s.pending_started_at > 0 ? s.pending_started_at : (run?.started_at ?? Date.now() / 1000);
|
|
1498
1846
|
}
|