@maudecode/talaria-web 1.16.1 → 1.18.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.
- package/_release.json +8 -8
- package/dist/api/auth-raw.d.ts +1 -1
- package/dist/api/auth-raw.js +4 -4
- package/dist/api/auth-raw.js.map +1 -1
- package/dist/api/automation-raw.d.ts +6 -0
- package/dist/api/automation-raw.js +59 -9
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +163 -10
- package/dist/api/automation-router.js +25 -8
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +700 -15
- package/dist/api/chat-router.js +74 -55
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +47 -2
- package/dist/api/git-router.js +31 -22
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +14 -0
- package/dist/api/raw-routes.js +277 -36
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +10 -0
- package/dist/api/router.js +10 -8
- package/dist/api/router.js.map +1 -1
- package/dist/api/sessions-router.d.ts +1710 -73
- package/dist/api/sessions-router.js +164 -10
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +132 -2
- package/dist/api/settings-router.js +15 -5
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +16 -3
- package/dist/api/sse-routes.js +194 -67
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.js +12 -25
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +425 -19
- package/dist/api/tools-router.js +157 -26
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +0 -9377
- package/dist/app.js +42 -17
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +8 -2
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/oidc.d.ts +5 -2
- package/dist/auth/oidc.js +8 -7
- package/dist/auth/oidc.js.map +1 -1
- package/dist/bin/talaria-web.js +1 -1
- package/dist/bin/talaria-web.js.map +1 -1
- package/dist/cli/dotenv.js +1 -0
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/config/agent-config.d.ts +2 -0
- package/dist/config/agent-config.js +12 -2
- package/dist/config/agent-config.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/http/context.d.ts +20 -3
- package/dist/http/context.js +41 -8
- package/dist/http/context.js.map +1 -1
- package/dist/http/csp.d.ts +2 -1
- package/dist/http/csp.js +3 -2
- package/dist/http/csp.js.map +1 -1
- package/dist/onboarding.js +4 -0
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.d.ts +10 -0
- package/dist/profiles/profiles.js +40 -2
- package/dist/profiles/profiles.js.map +1 -1
- package/dist/providers/catalog.d.ts +101 -8
- package/dist/providers/catalog.js +463 -123
- package/dist/providers/catalog.js.map +1 -1
- package/dist/raster-image.d.ts +7 -0
- package/dist/raster-image.js +267 -0
- package/dist/raster-image.js.map +1 -0
- package/dist/redact.d.ts +1 -0
- package/dist/redact.js +354 -44
- package/dist/redact.js.map +1 -1
- package/dist/runtime.js +111 -27
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +2 -0
- package/dist/sessions/anchor.js +8 -3
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-tasks.d.ts +2 -9
- package/dist/sessions/background-tasks.js +2 -12
- package/dist/sessions/background-tasks.js.map +1 -1
- package/dist/sessions/claude-code.d.ts +19 -0
- package/dist/sessions/claude-code.js +250 -0
- package/dist/sessions/claude-code.js.map +1 -0
- package/dist/sessions/cli-sessions.d.ts +15 -4
- package/dist/sessions/cli-sessions.js +120 -29
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +61 -9
- package/dist/sessions/completions.js +282 -34
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/compress.d.ts +14 -1
- package/dist/sessions/compress.js +52 -11
- package/dist/sessions/compress.js.map +1 -1
- package/dist/sessions/export.d.ts +5 -0
- package/dist/sessions/export.js +30 -0
- package/dist/sessions/export.js.map +1 -1
- package/dist/sessions/gateway-watcher.d.ts +7 -3
- package/dist/sessions/gateway-watcher.js +31 -19
- package/dist/sessions/gateway-watcher.js.map +1 -1
- package/dist/sessions/handoff.d.ts +32 -0
- package/dist/sessions/handoff.js +114 -0
- package/dist/sessions/handoff.js.map +1 -0
- package/dist/sessions/journal.d.ts +8 -0
- package/dist/sessions/journal.js +44 -10
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +13 -3
- package/dist/sessions/list.js +46 -15
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +53 -5
- package/dist/sessions/merge.js +219 -23
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/recovery.d.ts +22 -0
- package/dist/sessions/recovery.js +506 -0
- package/dist/sessions/recovery.js.map +1 -0
- package/dist/sessions/relay.d.ts +21 -2
- package/dist/sessions/relay.js +156 -11
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +152 -8
- package/dist/sessions/service.js +640 -72
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +17 -1
- package/dist/sessions/session.js +63 -4
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/state-db-worker.d.ts +1 -0
- package/dist/sessions/state-db-worker.js +33 -0
- package/dist/sessions/state-db-worker.js.map +1 -0
- package/dist/sessions/state-db.d.ts +87 -4
- package/dist/sessions/state-db.js +417 -8
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/store.d.ts +7 -0
- package/dist/sessions/store.js +36 -22
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +10 -0
- package/dist/sessions/streams.js +14 -3
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/titles.d.ts +2 -0
- package/dist/sessions/titles.js +4 -0
- package/dist/sessions/titles.js.map +1 -1
- package/dist/sessions/todo.d.ts +6 -1
- package/dist/sessions/todo.js +11 -4
- package/dist/sessions/todo.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +12 -1
- package/dist/sessions/tool-display.js +50 -1
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +1 -1
- package/dist/sessions/turn-context.js +11 -7
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +67 -11
- package/dist/sessions/turn.js +413 -111
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/wakeup-pause.d.ts +59 -0
- package/dist/sessions/wakeup-pause.js +181 -0
- package/dist/sessions/wakeup-pause.js.map +1 -0
- package/dist/settings.d.ts +20 -2
- package/dist/settings.js +45 -9
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/fake.d.ts +4 -4
- package/dist/sidecar/fake.js +5 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/spa.js +2 -1
- package/dist/spa.js.map +1 -1
- package/dist/startup.d.ts +13 -5
- package/dist/startup.js +48 -0
- package/dist/startup.js.map +1 -1
- package/dist/test/harness.js +2 -0
- package/dist/test/harness.js.map +1 -1
- package/dist/test/ts-worker-hooks.d.ts +1 -0
- package/dist/test/ts-worker-hooks.js +24 -0
- package/dist/test/ts-worker-hooks.js.map +1 -0
- package/dist/text/diff.d.ts +9 -14
- package/dist/text/diff.js +74 -108
- package/dist/text/diff.js.map +1 -1
- package/dist/tools/crons.js +45 -8
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +53 -0
- package/dist/tools/dashboard-plugins.js +188 -0
- package/dist/tools/dashboard-plugins.js.map +1 -0
- package/dist/tools/extensions.d.ts +13 -1
- package/dist/tools/extensions.js +132 -60
- package/dist/tools/extensions.js.map +1 -1
- package/dist/tools/health.d.ts +32 -0
- package/dist/tools/health.js +111 -19
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/insights.d.ts +8 -2
- package/dist/tools/insights.js +18 -2
- package/dist/tools/insights.js.map +1 -1
- package/dist/tools/kanban.d.ts +41 -1
- package/dist/tools/kanban.js +113 -2
- package/dist/tools/kanban.js.map +1 -1
- package/dist/tools/mcp.d.ts +15 -1
- package/dist/tools/mcp.js +99 -5
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/project-os.d.ts +11 -0
- package/dist/tools/project-os.js +267 -0
- package/dist/tools/project-os.js.map +1 -0
- package/dist/tools/skills.d.ts +5 -1
- package/dist/tools/skills.js +5 -3
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/terminal.d.ts +1 -0
- package/dist/tools/terminal.js +1 -1
- package/dist/tools/terminal.js.map +1 -1
- package/dist/tools/updates.d.ts +33 -8
- package/dist/tools/updates.js +236 -57
- package/dist/tools/updates.js.map +1 -1
- package/dist/tools/wiki.d.ts +68 -0
- package/dist/tools/wiki.js +413 -0
- package/dist/tools/wiki.js.map +1 -0
- package/dist/workspace/escape.d.ts +61 -0
- package/dist/workspace/escape.js +149 -0
- package/dist/workspace/escape.js.map +1 -0
- package/dist/workspace/extract.d.ts +22 -0
- package/dist/workspace/extract.js +308 -0
- package/dist/workspace/extract.js.map +1 -0
- package/dist/workspace/fs.d.ts +19 -3
- package/dist/workspace/fs.js +96 -23
- package/dist/workspace/fs.js.map +1 -1
- package/dist/workspace/git.d.ts +18 -0
- package/dist/workspace/git.js +38 -25
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media.d.ts +4 -4
- package/dist/workspace/media.js +18 -45
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/preview.d.ts +28 -0
- package/dist/workspace/preview.js +110 -0
- package/dist/workspace/preview.js.map +1 -0
- package/dist/workspace/rollback.d.ts +36 -21
- package/dist/workspace/rollback.js +179 -270
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/unzip.js +4 -1
- package/dist/workspace/unzip.js.map +1 -1
- package/dist/workspace/upload.d.ts +9 -4
- package/dist/workspace/upload.js +9 -3
- package/dist/workspace/upload.js.map +1 -1
- package/dist/workspace/worktrees.d.ts +2 -1
- package/dist/workspace/worktrees.js +14 -15
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +4 -2
- package/sidecar/agent_dependency.json +4 -4
- package/sidecar/scripts/replay_sidecar.py +25 -1
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/__main__.py +7 -1
- package/sidecar/talaria_sidecar/home.py +52 -2
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +39 -5
- package/sidecar/talaria_sidecar/methods/chat.py +431 -43
- package/sidecar/talaria_sidecar/methods/checkpoints.py +89 -0
- package/sidecar/talaria_sidecar/methods/commands.py +96 -2
- package/sidecar/talaria_sidecar/methods/cron.py +39 -8
- package/sidecar/talaria_sidecar/methods/gateway.py +32 -15
- package/sidecar/talaria_sidecar/methods/kanban.py +140 -19
- package/sidecar/talaria_sidecar/methods/mcp.py +1 -1
- package/sidecar/talaria_sidecar/methods/process.py +115 -2
- package/sidecar/talaria_sidecar/methods/profiles.py +84 -8
- package/sidecar/talaria_sidecar/methods/providers.py +27 -8
- package/sidecar/talaria_sidecar/methods/runtime.py +3 -0
- package/sidecar/talaria_sidecar/methods/state_db.py +39 -5
- package/sidecar/talaria_sidecar/methods/text.py +17 -1
- package/sidecar/talaria_sidecar/methods/usage.py +257 -4
- package/sidecar/talaria_sidecar/runtime.py +17 -1
- package/static/dist/FILES.txt +127 -125
- package/static/dist/assets/AppShell-g3bkgnvt.js +1 -0
- package/static/dist/assets/ChatPage-oo7bdan6.js +25 -0
- package/static/dist/assets/Dialog-g355qxhp.js +1 -0
- package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +1 -0
- package/static/dist/assets/HubRoute-lmkrbw47.js +2 -0
- package/static/dist/assets/{Markdown-jqrx5es0.js → Markdown-j4kut4j2.js} +125 -125
- package/static/dist/assets/RightPanel-ohlsdzfl.js +1 -0
- package/static/dist/assets/Select-kddq75qc.js +1 -0
- package/static/dist/assets/SessionListPanel-lu3oy0hw.js +1 -0
- package/static/dist/assets/SettingsLayout-o9bv4mgb.js +1 -0
- package/static/dist/assets/States-eq6fe29m.js +1 -0
- package/static/dist/assets/Toaster-pc247u9f.js +1 -0
- package/static/dist/assets/_app-f8n1i0m1.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-gfbzpoou.js +1 -0
- package/static/dist/assets/_app.index-bzzv80hk.js +1 -0
- package/static/dist/assets/_app.insights-dzixbryi.js +1 -0
- package/static/dist/assets/_app.kanban-c6hep2us.js +1 -0
- package/static/dist/assets/_app.logs-i69qds98.js +1 -0
- package/static/dist/assets/_app.memory-kl4vf0gd.js +1 -0
- package/static/dist/assets/_app.profiles-b8zinmyp.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-hh27wwu2.js → _app.session._sessionId-fn1bhde6.js} +1 -1
- package/static/dist/assets/_app.settings-cg8plwmf.js +1 -0
- package/static/dist/assets/_app.settings._section-bkwbi9zn.js +3 -0
- package/static/dist/assets/_app.skills-cs47sbvs.js +1 -0
- package/static/dist/assets/_app.tasks-dwvk64tc.js +1 -0
- package/static/dist/assets/_app.todos-c8xjm8p3.js +1 -0
- package/static/dist/assets/_app.workspaces-ehvd1gds.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-oity9y4o.js → abnfDiagram-VCTEODGH-c8kdixke.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-ceaepnyr.js → architectureDiagram-5GKGNRK7-cku06x1p.js} +1 -1
- package/static/dist/assets/{back-eu64w89w.js → back-h7aun7ew.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-n79st4pj.js → blockDiagram-I7D4REHJ-jzthp1wa.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-mi73akzk.js → c4Diagram-7LVT6UL2-mwr67mt8.js} +1 -1
- package/static/dist/assets/{cancel-mzvkdjg8.js → cancel-fhi0vcnu.js} +1 -1
- package/static/dist/assets/channel-b2zo73z1.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-laat0g1q.js → chunk-4HAMMTFA-gpq1y228.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-0psy1tr2.js → chunk-75Z2AOVW-ktje58c4.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-lw8uox8x.js → chunk-DU6HZSFF-ofajdixh.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-erassib3.js → chunk-F27PBJKO-cmev85ti.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-fz7rx619.js → chunk-GMAD6QVW-htjgt22y.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-gdb0epb2.js → chunk-GVQU2GXP-qndvoxl7.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-ea8t26qw.js → chunk-IMKFNOWR-e5bkd5q6.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-mb1gu9kz.js → chunk-L3NEJ4N5-mj1155r6.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-eovmccvy.js → chunk-OSK3NFVY-e7gkikcw.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-bo55zjo5.js → chunk-P2QGCYS3-f7t54xfm.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-ja7ohe4h.js → chunk-POPQ4Y6H-i927v9oa.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-e30g9nus.js → chunk-PWAF6VOD-m84lmnyt.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-gbb798f7.js → chunk-SHT3W25Y-mi0tlcu9.js} +1 -1
- package/static/dist/assets/{chunk-SVP7TREG-h2iw8urm.js → chunk-SVP7TREG-dnau702j.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-fd2cottz.js → chunk-TICWLB2K-edem1gkg.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +1 -0
- package/static/dist/assets/{copied-h65fc3cx.js → copied-hcplv46p.js} +1 -1
- package/static/dist/assets/copy-magtofyp.js +1 -0
- package/static/dist/assets/{cynefin-OW5HDTMX-lbltyl5j.js → cynefin-OW5HDTMX-siqlulkl.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-lxhsqq4a.js → cynefinDiagram-5FMLGOSQ-i648tah9.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-gn4nnaln.js → dagre-GXQ25YYZ-mwn6uc17.js} +1 -1
- package/static/dist/assets/{diagram-S7CK7UJ4-pa3ij6q7.js → diagram-S7CK7UJ4-l8abxaes.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-zbipyz57.js → diagram-UQ7AKVKN-mekq7zgq.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-d1kbpzgf.js → diagram-VSXAHHWV-fnky6lvc.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-i3ty14xl.js → diagram-VX7I27RA-xe6r5qfw.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-m8xefqa7.js → diagram-Z3DM3KII-nq07dg7k.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-nz7wj05l.js → ebnfDiagram-PWID7BFC-c62ipm3g.js} +1 -1
- package/static/dist/assets/endpoints-klbxod4t.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-fa41ib83.js → erDiagram-RLTQ6QDP-h39kmnzy.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-n7zq01qu.js → ganttDiagram-EL5Y4UJY-ngbsyp37.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-c89emb07.js → gitGraph-4MIJSDKK-fr2i7caa.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-cz7cgr6w.js → gitGraphDiagram-WWUBYQGX-eravdti4.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-d9bapmni.js → highlighted-body-KPVGNVTW-jwpop9p8.js} +1 -1
- package/static/dist/assets/host-i828xuo7.js +1 -0
- package/static/dist/assets/index-eh8zilnv.css +2 -0
- package/static/dist/assets/index-r648f4o1.js +11 -0
- package/static/dist/assets/{info-A6RAGUB7-ndddhhhh.js → info-A6RAGUB7-h11x0dxo.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-ccxigcpf.js → infoDiagram-27XIBGKW-dfh47dw9.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-le0z9vtc.js → ishikawaDiagram-5VMMS53U-i6ypq351.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-ivsmb26o.js → journeyDiagram-3NMN7TZE-lduf1z95.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-h6n2awt0.js → kanban-definition-UXKFOSKX-e18bu4pg.js} +1 -1
- package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +1 -0
- package/static/dist/assets/{line-dmzlfe0u.js → line-meizqsvn.js} +1 -1
- package/static/dist/assets/link-oot2yn73.js +1 -0
- package/static/dist/assets/login-gu28l9ou.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-gwyd5bnq.js → mermaid-parser.core-ntne45i9.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-l68gg5h7.js → mindmap-definition-YA3MSWOX-27b26eok.js} +1 -1
- package/static/dist/assets/onboarding-lkphzu1s.js +1 -0
- package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-gpv06065.js → packet-AYTQ26CC-l6jdka8p.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-e52vs23u.js → pegDiagram-XKGWAZYB-g1er3bxj.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-l6m7nz5s.js → pie-WAS4IAKB-i0ylcrt9.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-b4is36bq.js → pieDiagram-E7YTZNPT-fbz1e55z.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-m8vt2788.js → quadrantDiagram-AXDQQJYC-hhue7d8o.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-nxbrhiov.js → radar-RG4KPBEZ-f9b70aa4.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-222mu9hi.js → railroad-74A4TZTK-lgsuoeda.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-o8lldfzv.js → railroadDiagram-O6MQD6OU-eqpy9fnz.js} +1 -1
- package/static/dist/assets/refresh-abtu93ke.js +1 -0
- package/static/dist/assets/refresh-cw-hlwb02qv.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-ovyq38kv.js → requirementDiagram-BXWQKSXE-ogf6puhi.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-ou7lznor.js → sankeyDiagram-P5KCCOFB-fqb8xcjf.js} +1 -1
- package/static/dist/assets/{saved-bazmcup9.js → saved-girnabkf.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-ovckdzf2.js → sequenceDiagram-WJ2MYXX4-dlcz6qjz.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-ertwwhlx.js → settings_section_extensions_title-h14cwk6f.js} +1 -1
- package/static/dist/assets/share._token-kl0r7ido.js +2 -0
- package/static/dist/assets/sliders-horizontal-n1zm34qq.js +1 -0
- package/static/dist/assets/sse-iwku0sjo.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-fcrexkkv.js → stateDiagram-D77RDMKH-fyoznpcs.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-f7usu8fw.js → swimlanes-42K2YHIH-oefldhfy.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-gp1hvz9f.js → timeline-definition-24CTP7MA-b1vl2zil.js} +1 -1
- package/static/dist/assets/todoStore-jjr5yr78.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-gb9gf11u.js → treeView-Q6P3EWNA-f7hcci5o.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-nuvfnvvi.js → treemap-WGGIJYW6-nl85071a.js} +1 -1
- package/static/dist/assets/units-kgxzxfhi.js +1 -0
- package/static/dist/assets/{untitled-htelhc68.js → untitled-cikfjntn.js} +1 -1
- package/static/dist/assets/{useForm-h2t0u14e.js → useForm-kqoqg0k9.js} +1 -1
- package/static/dist/assets/useLocale-o59tbiea.js +1 -0
- package/static/dist/assets/{useParams-dqd64jv7.js → useParams-b4dq6n4q.js} +1 -1
- package/static/dist/assets/{useSearch-j30pvsy7.js → useSearch-ipfmv3he.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-fzchbzaj.js → vennDiagram-4TSXK5OY-h5hzbk0n.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-m47mhfbh.js → wardley-WFR3VGLG-jftvpm3e.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-odg6ct1z.js → wardleyDiagram-VM6X3IG4-hedh85ge.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-mrhms5um.js → xychartDiagram-S5SC5T6Z-e49523ab.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +2 -2
- package/static/dist/assets/AppShell-ef3bhu32.js +0 -1
- package/static/dist/assets/Button-nx37f4u1.js +0 -1
- package/static/dist/assets/ChatPage-cdcpykcg.js +0 -25
- package/static/dist/assets/Dialog-mec42hvb.js +0 -1
- package/static/dist/assets/HubRoute-ecb65zpr.js +0 -2
- package/static/dist/assets/RightPanel-d1lgvk4s.js +0 -1
- package/static/dist/assets/Select-h4ivl9qf.js +0 -1
- package/static/dist/assets/SessionListPanel-ewib046b.js +0 -1
- package/static/dist/assets/SettingsLayout-lx654cup.js +0 -1
- package/static/dist/assets/States-ehsb6lzo.js +0 -1
- package/static/dist/assets/Toaster-mch1pa3t.js +0 -1
- package/static/dist/assets/_app-m8d961ef.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-mp33hbzi.js +0 -1
- package/static/dist/assets/_app.index-dk6zp7e0.js +0 -1
- package/static/dist/assets/_app.insights-jt2fk4i1.js +0 -1
- package/static/dist/assets/_app.kanban-if4pubeu.js +0 -1
- package/static/dist/assets/_app.logs-mfaw9m6g.js +0 -1
- package/static/dist/assets/_app.memory-h3eaarb6.js +0 -1
- package/static/dist/assets/_app.profiles-ienktovs.js +0 -1
- package/static/dist/assets/_app.settings-dpoe0bzf.js +0 -1
- package/static/dist/assets/_app.settings._section-l468ynag.js +0 -3
- package/static/dist/assets/_app.skills-kcbncc8i.js +0 -1
- package/static/dist/assets/_app.tasks-occdrqhw.js +0 -2
- package/static/dist/assets/_app.todos-nnke2rcc.js +0 -1
- package/static/dist/assets/_app.workspaces-cdlatvwj.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-hj11333z.js +0 -1
- package/static/dist/assets/channel-mbvmf2vv.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-o8r05k0n.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-o8r05k0n.js +0 -1
- package/static/dist/assets/copy-e6977uwq.js +0 -1
- package/static/dist/assets/endpoints-k7yf25m0.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-kpbq09bx.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-lcz0kx14.js +0 -1
- package/static/dist/assets/index-h00usru2.js +0 -11
- package/static/dist/assets/index-n73ijkv2.css +0 -2
- package/static/dist/assets/lazyRouteComponent-m1rtyk4t.js +0 -1
- package/static/dist/assets/link-budeg5t9.js +0 -1
- package/static/dist/assets/login-kjhasvp8.js +0 -1
- package/static/dist/assets/matchContext-f8oy3lo7.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-jb5d211j.js +0 -1
- package/static/dist/assets/onboarding-cdfrk0pj.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-kdvgl6x8.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-itiqslby.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-go7kov78.js +0 -1
- package/static/dist/assets/refresh-cw-kbcje1uy.js +0 -1
- package/static/dist/assets/search-jen74z4u.js +0 -1
- package/static/dist/assets/share._token-dan9hgvj.js +0 -2
- package/static/dist/assets/sliders-horizontal-f9cb3lh3.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-lcfraua2.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ogmanni0.js +0 -8
- package/static/dist/assets/todoStore-eqtid4r4.js +0 -1
- package/static/dist/assets/useLocale-hcsb84pj.js +0 -1
- package/static/dist/assets/useNavigate-nwy70ztj.js +0 -1
package/dist/sessions/turn.js
CHANGED
|
@@ -12,29 +12,54 @@ import { isWithin, resolvePathLikePython } from '../workspace/paths.js';
|
|
|
12
12
|
import { SidecarError } from '../sidecar/client.js';
|
|
13
13
|
import { HttpFailure, markSessionTitleGenerated } from './service.js';
|
|
14
14
|
import { StreamRegistry, SessionChannels } from './streams.js';
|
|
15
|
+
import { ChatUsageSchema } from '@maudecode/talaria-web-contracts';
|
|
15
16
|
import { PendingPrompts, clarifyReply } from './pending.js';
|
|
16
17
|
import { RunJournal } from './journal.js';
|
|
17
|
-
import { CONTEXT_USAGE_FIELDS, Session, titleFrom } from './session.js';
|
|
18
|
-
import {
|
|
18
|
+
import { COMPRESSION_RECOVERY_ACTION_START_FOCUSED, COMPRESSION_RECOVERY_TERMINAL_STATE, CONTEXT_USAGE_FIELDS, Session, contextUsage, titleFrom } from './session.js';
|
|
19
|
+
import { anchorMessageKey, compressionReference, markerSummary, visibleMessagesForAnchor } from './compress.js';
|
|
20
|
+
import { buildActiveTurnToken, completedToolIndex, publicToolFrame, redactNestedMessageContainers, redactSessionData, redactString, withToolId } from '../redact.js';
|
|
19
21
|
import { dict } from '../config/agent-config.js';
|
|
20
22
|
import { ReasoningTitleTracker, reasoningEventPayload } from './reasoning-titles.js';
|
|
21
23
|
import { messageWindowForDisplay, messagesForLimitedPayload, toolCallsForMessageWindow } from './window.js';
|
|
22
|
-
import { attachTodoState } from './todo.js';
|
|
24
|
+
import { UNSETTLED_TODO_KEY, attachTodoState, parseTodoToolResult } from './todo.js';
|
|
23
25
|
import { withSessionWireFlags } from './list.js';
|
|
24
26
|
import { hydrateAnchorActivityScenes, turnTerminalState, withTurnIds } from './anchor.js';
|
|
25
27
|
import { persistentStateChanges, persistentStateSnapshot } from './state-saved.js';
|
|
26
28
|
import { maxIterationsFromConfig, maxTokensFromConfig, processWakeupMaxIterations, reasoningConfigFromConfig, webuiEphemeralSystemPrompt, workspaceSystemMessage } from './turn-context.js';
|
|
27
|
-
import { agentSteerText, assistantReplyAddedAfterCurrentTurn, buildPartialMessage,
|
|
29
|
+
import { agentSteerText, assistantReplyAddedAfterCurrentTurn, attachedFilesPrompt, buildPartialMessage, dedupeContext, checkpointTurnStart, extractToolCallsFromMessages, hasNativeImages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, sessionLacksFinalAssistantAnswer, splitDisplayText, stateDbSeenId, joinReasoning, reasoningFieldsText, toolOutcome, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withToolCallOutcomes, withoutMaxIterationSummaryRequest, withNativeImagesRestored, withoutNativeImages, withoutToolImages } from './merge.js';
|
|
28
30
|
import { mayBecomeSilentReply, turnOrigin, withBackgroundUpdates } from './background-updates.js';
|
|
29
|
-
import { fallbackTitleFromExchange, firstExchangeSnippets, isGenericFallbackTitle, latestExchangeSnippets, looksInvalidGeneratedTitle, looksLikeDefaultCliTitle, sanitizeGeneratedTitle, titleLanguageMismatch, titlePrompts } from './titles.js';
|
|
31
|
+
import { countExchanges, fallbackTitleFromExchange, firstExchangeSnippets, isGenericFallbackTitle, latestExchangeSnippets, looksInvalidGeneratedTitle, looksLikeDefaultCliTitle, sanitizeGeneratedTitle, titleLanguageMismatch, titlePrompts } from './titles.js';
|
|
30
32
|
import { WORKSPACE_BUSY_MESSAGE } from '../workspace/git.js';
|
|
33
|
+
import { CHECKPOINT_STORE_BUSY_MESSAGE } from '../workspace/rollback.js';
|
|
31
34
|
import { str } from '../util.js';
|
|
35
|
+
import { toolEditDiff } from './tool-display.js';
|
|
36
|
+
import { recordWakeupPause } from './wakeup-pause.js';
|
|
32
37
|
export const CHAT_LOCK_WAIT_SECONDS = 2;
|
|
33
38
|
const IMAGE_MODE_TIMEOUT_MS = 15_000;
|
|
34
39
|
const TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT = 80;
|
|
40
|
+
/** TAL-397: streamed text reports the live meter at most this often (seconds); a counter change reports at once. */
|
|
41
|
+
const METERING_INTERVAL_S = 1;
|
|
35
42
|
/** How long a user's message waits for a stopped background turn to unwind before its own turn is admitted. */
|
|
36
43
|
const BACKGROUND_UNWIND_WAIT_MS = 30_000;
|
|
37
44
|
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.' };
|
|
45
|
+
/** The Agent's session counters onto a session's: a zero or missing reading keeps the figure it already has. */
|
|
46
|
+
function applyAgentUsage(target, usage) {
|
|
47
|
+
if (usage.prompt_tokens > 0)
|
|
48
|
+
target.input_tokens = usage.prompt_tokens;
|
|
49
|
+
if (usage.completion_tokens > 0)
|
|
50
|
+
target.output_tokens = usage.completion_tokens;
|
|
51
|
+
if (usage.estimated_cost_usd !== null)
|
|
52
|
+
target.estimated_cost = usage.estimated_cost_usd;
|
|
53
|
+
if (usage.cache_read_tokens > 0)
|
|
54
|
+
target.cache_read_tokens = usage.cache_read_tokens;
|
|
55
|
+
if (usage.cache_write_tokens > 0)
|
|
56
|
+
target.cache_write_tokens = usage.cache_write_tokens;
|
|
57
|
+
}
|
|
58
|
+
/** TAL-397: the `metering` frame's counters, the same figures the session persists. */
|
|
59
|
+
const meteringUsage = (c) => ({
|
|
60
|
+
input_tokens: c.input_tokens || 0, output_tokens: c.output_tokens || 0, estimated_cost: typeof c.estimated_cost === 'number' ? c.estimated_cost : null,
|
|
61
|
+
cache_read_tokens: c.cache_read_tokens || 0, cache_write_tokens: c.cache_write_tokens || 0,
|
|
62
|
+
});
|
|
38
63
|
/** `compressionExhausted`: the Agent flagged the turn itself, so its wording does not matter. */
|
|
39
64
|
export function classifyProviderError(errStr, opts = {}) {
|
|
40
65
|
const lower = errStr.toLowerCase();
|
|
@@ -48,7 +73,7 @@ export function classifyProviderError(errStr, opts = {}) {
|
|
|
48
73
|
if (['interrupted by user', 'response interrupted', 'operation interrupted', 'operation was interrupted', 'operation aborted', 'request was aborted', 'aborterror'].some((k) => lower.includes(k))) {
|
|
49
74
|
return { label: 'Response interrupted', type: 'interrupted', hint: 'The run stopped before a provider response completed. If you did not cancel it, try again.' };
|
|
50
75
|
}
|
|
51
|
-
const quota = ['insufficient_quota', 'insufficient credits', 'out of credits', 'credits exhausted', 'quota exceeded', 'usage limit', 'plan limit', 'billing'].some((k) => lower.includes(k));
|
|
76
|
+
const quota = ['insufficient_quota', 'insufficient credits', 'out of credits', 'credits exhausted', 'quota exceeded', 'usage limit', 'plan limit', 'billing', 'more credits', 'can only afford', 'fewer max_tokens', 'credit balance', 'exceeded your current quota', 'usage_limit_exceeded', 'reached the limit of messages', 'used up your usage'].some((k) => lower.includes(k));
|
|
52
77
|
const poolEmpty = lower.includes('credential(s) exhausted') || lower.includes('credentials exhausted') || (lower.includes('credential') && lower.includes('exhausted'));
|
|
53
78
|
const auth = !quota && !poolEmpty && (errStr.includes('401') || lower.includes('authentication') || lower.includes('unauthorized') || lower.includes('invalid api key') || lower.includes('invalid_api_key') || lower.includes('no cookie auth credentials'));
|
|
54
79
|
const notFound = errStr.includes('404') || lower.includes('not found') || lower.includes('does not exist') || lower.includes('model_not_found') || lower.includes('invalid model') || lower.includes('does not match any known model') || lower.includes('unknown model');
|
|
@@ -102,8 +127,6 @@ export function sniffImageMime(bytes, declared = '') {
|
|
|
102
127
|
return 'image/bmp';
|
|
103
128
|
return null;
|
|
104
129
|
}
|
|
105
|
-
const COMPRESSION_RECOVERY_TERMINAL_STATE = 'compression_exhausted';
|
|
106
|
-
const COMPRESSION_RECOVERY_ACTION_START_FOCUSED = 'start_focused_continuation';
|
|
107
130
|
/** Python `build_compression_recovery_payload` + `stamp_compression_exhausted_recovery`. */
|
|
108
131
|
export function stampCompressionExhaustedRecovery(s, message, details) {
|
|
109
132
|
const positive = (v) => { const n = Math.trunc(Number(v) || 0); return n > 0 ? n : 0; };
|
|
@@ -137,7 +160,27 @@ export function isGenericContinuationIntent(text) {
|
|
|
137
160
|
const parts = normalized.split(' ');
|
|
138
161
|
return parts.length > 0 && parts.length <= 2 && parts.every((p) => GENERIC_CONTINUATION_INTENTS.has(p));
|
|
139
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Python `_aux_title_generation_enabled`: `auxiliary.title_generation.enabled` with the Agent's
|
|
165
|
+
* `is_truthy_value(default=True)` semantics. Missing/null enables; a string enables only as 1/true/yes/on.
|
|
166
|
+
*/
|
|
167
|
+
export function titleGenerationEnabled(cfg) {
|
|
168
|
+
const value = dict(dict(cfg.auxiliary).title_generation).enabled;
|
|
169
|
+
if (value === undefined || value === null)
|
|
170
|
+
return true;
|
|
171
|
+
if (typeof value === 'string')
|
|
172
|
+
return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
|
|
173
|
+
// Python `bool()`: empty containers are false; any other number than zero (NaN included) is true.
|
|
174
|
+
if (Array.isArray(value))
|
|
175
|
+
return value.length > 0;
|
|
176
|
+
if (typeof value === 'object')
|
|
177
|
+
return Object.keys(value).length > 0;
|
|
178
|
+
if (typeof value === 'number')
|
|
179
|
+
return value !== 0;
|
|
180
|
+
return Boolean(value);
|
|
181
|
+
}
|
|
140
182
|
/** Python `_explicit_text_signal`: `agent.image_input_mode: text` or a configured `auxiliary.vision` backend. */
|
|
183
|
+
const isImageAttachment = (att) => Boolean(str(att.path).trim()) && str(att.mime).trim().startsWith('image/');
|
|
141
184
|
export function explicitTextSignal(cfg) {
|
|
142
185
|
const agent = dict(cfg.agent);
|
|
143
186
|
if (str(agent.image_input_mode ?? 'auto').trim().toLowerCase() === 'text')
|
|
@@ -162,8 +205,14 @@ export class TurnRunner {
|
|
|
162
205
|
steerRewrites = new Map();
|
|
163
206
|
deferredPendingSteer = new Map();
|
|
164
207
|
lastCompletedTool = new Map();
|
|
208
|
+
/** TAL-397: each running turn's live counters, once the Agent reported any; an error or cancel persists them too. */
|
|
209
|
+
liveUsage = new Map();
|
|
210
|
+
/** TAL-397: each running turn's latest todo list, which an error or cancel keeps on the session. */
|
|
211
|
+
liveTodos = new Map();
|
|
165
212
|
/** Streams whose run completed (`done` emitted) and only await title work; a late cancel is a no-op for these. */
|
|
166
213
|
settledStreams = new Set();
|
|
214
|
+
/** The last adaptive title refresh per session; a successor turn's refresh runs after it (TAL-589). */
|
|
215
|
+
titleRefreshes = new Map();
|
|
167
216
|
/**
|
|
168
217
|
* TAL-364: what a Stop needs to write the turn's model context: its starting context, the prompt the Agent got, and
|
|
169
218
|
* whether a settlement already used a canonical checkpoint (which marks the stop boundary; later work never replaces it).
|
|
@@ -187,6 +236,8 @@ export class TurnRunner {
|
|
|
187
236
|
return { error: `Profile '${str(s.profile)}' is being deleted.`, _status: 409 };
|
|
188
237
|
if (this.deps.workspaceBusy?.(opts.workspace))
|
|
189
238
|
return { error: WORKSPACE_BUSY_MESSAGE, _status: 409 };
|
|
239
|
+
if (this.deps.checkpointStoreBusy?.(s.profile ?? null))
|
|
240
|
+
return { error: CHECKPOINT_STORE_BUSY_MESSAGE, _status: 409 };
|
|
190
241
|
const locked = s.active_stream_id;
|
|
191
242
|
if (locked) {
|
|
192
243
|
if (this.registry.liveIds.has(locked) || this.registry.activeRuns.has(locked))
|
|
@@ -244,6 +295,10 @@ export class TurnRunner {
|
|
|
244
295
|
response.effective_model_provider = opts.modelProvider;
|
|
245
296
|
return response;
|
|
246
297
|
}
|
|
298
|
+
/** Resolves once the stream's worker has ended (settled, failed, or torn down); it never rejects. */
|
|
299
|
+
finished(streamId) {
|
|
300
|
+
return this.workers.get(streamId) ?? Promise.resolve();
|
|
301
|
+
}
|
|
247
302
|
/** Python `_checkpoint_user_message_for_eager_session_save`. */
|
|
248
303
|
checkpointUserMessage(s, msg, attachments, startedAt, source, turnId) {
|
|
249
304
|
const latest = s.messages[s.messages.length - 1];
|
|
@@ -262,7 +317,12 @@ export class TurnRunner {
|
|
|
262
317
|
catch (error) {
|
|
263
318
|
deps.log(`[webui] WARNING: run journal degraded for stream ${streamId}: ${error.message}`);
|
|
264
319
|
}
|
|
320
|
+
const failure = { apperror: null, cancelled: false };
|
|
265
321
|
const put = (event, data, meta = {}) => {
|
|
322
|
+
if (event === 'apperror')
|
|
323
|
+
failure.apperror = data;
|
|
324
|
+
else if (event === 'cancel')
|
|
325
|
+
failure.cancelled = true;
|
|
266
326
|
for (const steerEvent of this.takeSteerEventsBefore(streamId, event))
|
|
267
327
|
put(steerEvent[0], steerEvent[1]);
|
|
268
328
|
if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn'].includes(event))
|
|
@@ -309,13 +369,16 @@ export class TurnRunner {
|
|
|
309
369
|
}
|
|
310
370
|
const msgText = opts.msg;
|
|
311
371
|
const previousMessages = structuredClone(s.messages);
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
372
|
+
// TAL-493: one state.db read builds the history and marks where this turn's own rows begin.
|
|
373
|
+
const startRead = deps.service().stateDbRead(s);
|
|
374
|
+
// TAL-578: an eager save already put this turn's prompt in the transcript; `user_message` carries it, so the history
|
|
375
|
+
// leaves it out.
|
|
376
|
+
const previousContext = structuredClone(deps.service().modelContext(s, startRead.rows)).filter((m) => m._turn_id !== streamId);
|
|
377
|
+
// Python `_sanitize_messages_for_agent`: the model never sees display-only rows or a replayed cancelled prompt, and
|
|
378
|
+
// the Agent gets back the exact bytes it sent (TAL-541).
|
|
379
|
+
const apiHistory = sanitizeMessagesForApi(previousContext, { preserveApiContent: true });
|
|
316
380
|
// Before the first await: a Stop can land at any point after admission.
|
|
317
|
-
|
|
318
|
-
this.stopContexts.set(streamId, { previousContext: previousContext.filter((m) => m._turn_id !== streamId), historyLength: apiHistory.length, prompt: workspaceCtx + msgText, msgText, checkpointed: false });
|
|
381
|
+
this.stopContexts.set(streamId, { previousContext, historyLength: apiHistory.length, prompt: msgText, msgText, checkpointed: false, stateDbStartId: startRead.ok ? stateDbSeenId(startRead.rows) ?? 0 : null });
|
|
319
382
|
const activeTurnToken = buildActiveTurnToken(streamId, s.pending_started_at);
|
|
320
383
|
const sidecar = deps.sidecar();
|
|
321
384
|
const partialText = this.registry.partialText.get(streamId) ?? [];
|
|
@@ -329,6 +392,25 @@ export class TurnRunner {
|
|
|
329
392
|
const toolStartedAt = new WeakMap();
|
|
330
393
|
let tokenSent = false;
|
|
331
394
|
let firstTokenAt = null;
|
|
395
|
+
// TAL-397: the live meter. Counters start from the session's and follow the Agent's; the rate counts text deltas.
|
|
396
|
+
const liveUsage = { input_tokens: s.input_tokens, output_tokens: s.output_tokens, estimated_cost: s.estimated_cost, cache_read_tokens: s.cache_read_tokens, cache_write_tokens: s.cache_write_tokens };
|
|
397
|
+
const meter = { deltas: 0, first: 0, last: 0, sentAt: null };
|
|
398
|
+
const putMetering = (force) => {
|
|
399
|
+
const now = deps.now();
|
|
400
|
+
if (!force && meter.sentAt !== null && now - meter.sentAt < METERING_INTERVAL_S)
|
|
401
|
+
return;
|
|
402
|
+
meter.sentAt = now;
|
|
403
|
+
const tps = meter.deltas && meter.last > meter.first ? Math.round((meter.deltas / (meter.last - meter.first)) * 10) / 10 : null;
|
|
404
|
+
put('metering', { session_id: sessionId, usage: meteringUsage(liveUsage), tps, tps_available: tps !== null, estimated: false });
|
|
405
|
+
};
|
|
406
|
+
const meterDelta = () => {
|
|
407
|
+
const now = deps.now();
|
|
408
|
+
if (!meter.deltas)
|
|
409
|
+
meter.first = now;
|
|
410
|
+
meter.last = now;
|
|
411
|
+
meter.deltas += 1;
|
|
412
|
+
putMetering(false);
|
|
413
|
+
};
|
|
332
414
|
// TAL-460: a background turn's text is held back while it could still be a silence marker, so one never flashes by.
|
|
333
415
|
// ponytail: checks the turn's whole text, so a marker after earlier prose streams until the settled row hides it;
|
|
334
416
|
// check the last text segment instead if that shows up.
|
|
@@ -338,7 +420,12 @@ export class TurnRunner {
|
|
|
338
420
|
let capturedTerminalError = null;
|
|
339
421
|
const controller = new AbortController();
|
|
340
422
|
this.abortControllers.set(streamId, controller);
|
|
341
|
-
const
|
|
423
|
+
const attachments = opts.attachments ?? [];
|
|
424
|
+
// TAL-545: one image mode decides both this turn's upload and the replayed history, which a text-mode turn sends
|
|
425
|
+
// without native images; only a turn with an image on either side asks for it.
|
|
426
|
+
const imageMode = attachments.some(isImageAttachment) || hasNativeImages(apiHistory) ? await this.imageInputMode(sessionId, s, opts, controller.signal) : null;
|
|
427
|
+
const userMessage = this.buildUserMessage(msgText, attachments, opts.workspace, sessionId, imageMode);
|
|
428
|
+
const conversationHistory = imageMode === 'text' ? withoutNativeImages(apiHistory) : apiHistory;
|
|
342
429
|
// The prompt the Agent actually gets, native image parts included.
|
|
343
430
|
const stop = this.stopContexts.get(streamId);
|
|
344
431
|
if (stop)
|
|
@@ -346,14 +433,15 @@ export class TurnRunner {
|
|
|
346
433
|
if (activeRun)
|
|
347
434
|
activeRun.phase = 'running';
|
|
348
435
|
const settledAt = { value: false };
|
|
349
|
-
let
|
|
436
|
+
let succeeded = false;
|
|
350
437
|
// TAL-424: steers an errored turn ended without taking, sent as one follow-up turn once this one is torn down.
|
|
351
438
|
let followUp = [];
|
|
352
439
|
try {
|
|
353
440
|
if (!sidecar)
|
|
354
441
|
throw new SidecarError('The Agent sidecar is not running; chat is unavailable until it starts.', { condition: 'sidecar_unavailable' });
|
|
355
442
|
// Python: budgets, reasoning config, personality and delivery context come from the profile's config.yaml; the
|
|
356
|
-
// system message carries the frozen session workspace, the WebUI guidance rides as the ephemeral prompt
|
|
443
|
+
// system message carries the frozen session workspace, the WebUI guidance rides as the ephemeral prompt with this
|
|
444
|
+
// turn's workspace (TAL-710: never a tag on the user's message, which the Agent can persist from memory).
|
|
357
445
|
const cfg = (await deps.profileConfig?.(s.profile ?? null)) ?? {};
|
|
358
446
|
// The sidecar's per-session YOLO state does not survive its restarts: push the local flag (either way) before
|
|
359
447
|
// the turn so a stale sidecar-side enable cannot auto-approve a session the UI reports as guarded.
|
|
@@ -368,10 +456,9 @@ export class TurnRunner {
|
|
|
368
456
|
put('cancel', this.cancelFrame(sessionId));
|
|
369
457
|
return;
|
|
370
458
|
}
|
|
371
|
-
const frozenWorkspace = str(s.created_workspace) || str(s.workspace);
|
|
372
459
|
const turnContext = {
|
|
373
|
-
system_message: workspaceSystemMessage(
|
|
374
|
-
ephemeral_system_prompt: webuiEphemeralSystemPrompt({ config: cfg, personality: str(s.personality) || null, sessionId, profile: s.profile ?? null, workspace:
|
|
460
|
+
system_message: workspaceSystemMessage(str(s.created_workspace) || str(s.workspace)),
|
|
461
|
+
ephemeral_system_prompt: webuiEphemeralSystemPrompt({ config: cfg, personality: str(s.personality) || null, sessionId, profile: s.profile ?? null, workspace: opts.workspace, hermesHome: deps.profileHome(s.profile), homeDisplay: deps.homeDisplay?.() ?? '~/.hermes', background: turnOrigin(opts.source) === 'background' }),
|
|
375
462
|
max_iterations: processWakeupMaxIterations(maxIterationsFromConfig(cfg), opts.source ?? 'webui', deps.env ?? {}),
|
|
376
463
|
max_tokens: maxTokensFromConfig(cfg),
|
|
377
464
|
reasoning_config: reasoningConfigFromConfig(cfg, opts.modelProvider ?? s.model_provider ?? null),
|
|
@@ -382,7 +469,7 @@ export class TurnRunner {
|
|
|
382
469
|
put('context_status', { session_id: sessionId, prefill: { status: 'not_configured', source: 'none', label: '', message_count: 0 } });
|
|
383
470
|
const result = await sidecar.call('chat.start', {
|
|
384
471
|
profile_home: deps.profileHome(s.profile), session_id: sessionId, stream_id: streamId, workspace: opts.workspace, model: opts.model ?? '', model_provider: opts.modelProvider,
|
|
385
|
-
user_message: userMessage, ...turnContext, conversation_history:
|
|
472
|
+
user_message: userMessage, ...turnContext, conversation_history: conversationHistory, enabled_toolsets: deps.toolsetsFor(s), ...(msgText ? { persist_user_message: msgText } : {}), ...(opts.ephemeral ? { ephemeral: true } : {}),
|
|
386
473
|
}, {
|
|
387
474
|
signal: controller.signal,
|
|
388
475
|
timeoutMs: 0,
|
|
@@ -399,10 +486,12 @@ export class TurnRunner {
|
|
|
399
486
|
partialText.push(str(data.text));
|
|
400
487
|
if (holdSilence && mayBecomeSilentReply(partialText.join(''))) {
|
|
401
488
|
heldText += str(data.text);
|
|
489
|
+
meterDelta();
|
|
402
490
|
return;
|
|
403
491
|
}
|
|
404
492
|
put('token', { text: heldText + str(data.text) });
|
|
405
493
|
heldText = '';
|
|
494
|
+
meterDelta();
|
|
406
495
|
return;
|
|
407
496
|
}
|
|
408
497
|
case 'reasoning': {
|
|
@@ -411,6 +500,16 @@ export class TurnRunner {
|
|
|
411
500
|
reasoningText.push(str(data.text));
|
|
412
501
|
// Python `reasoning_event_payload`: bold or first-line titles ride on the delta they complete.
|
|
413
502
|
put('reasoning', titles.prepare(reasoningEventPayload(str(data.text), reasoningText.join(''))));
|
|
503
|
+
meterDelta();
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
case 'usage': {
|
|
507
|
+
const usage = ChatUsageSchema.safeParse(data);
|
|
508
|
+
if (usage.success) {
|
|
509
|
+
applyAgentUsage(liveUsage, usage.data);
|
|
510
|
+
this.liveUsage.set(streamId, liveUsage);
|
|
511
|
+
}
|
|
512
|
+
putMetering(true);
|
|
414
513
|
return;
|
|
415
514
|
}
|
|
416
515
|
case 'steer_pending':
|
|
@@ -433,23 +532,34 @@ export class TurnRunner {
|
|
|
433
532
|
case 'tool_complete': {
|
|
434
533
|
// TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
|
|
435
534
|
// are its display sections, which keep the stderr and exit code the flat preview drops.
|
|
436
|
-
const { raw_result: rawResult, ...complete } = data;
|
|
535
|
+
const { raw_result: rawResult, todo_result: todoResult, result_diff: resultDiff, ...complete } = data;
|
|
437
536
|
const outcome = toolOutcome(rawResult);
|
|
438
537
|
complete.is_error = outcome.is_error;
|
|
439
538
|
if (rawResult !== undefined)
|
|
440
539
|
complete.result_view = outcome.result_view;
|
|
540
|
+
// TAL-448: a file edit's change, counted over the whole diff the sidecar forwards; redacted with the frame.
|
|
541
|
+
const editDiff = toolEditDiff(data.name, { diff: resultDiff });
|
|
542
|
+
if (editDiff)
|
|
543
|
+
complete.edit_diff = editDiff;
|
|
441
544
|
const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
|
|
442
545
|
if (tc) {
|
|
443
546
|
const startedAt = toolStartedAt.get(tc);
|
|
444
547
|
if (startedAt !== undefined)
|
|
445
548
|
complete.duration = Math.round(Math.max(0, deps.now() - startedAt) * 1000) / 1000;
|
|
446
549
|
// 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 } : {}) });
|
|
550
|
+
Object.assign(tc, { done: true, snippet: data.preview, is_error: complete.is_error, duration: complete.duration ?? null, ...(complete.result_view ? { result_view: complete.result_view } : {}), ...(editDiff ? { edit_diff: editDiff } : {}) });
|
|
448
551
|
}
|
|
449
552
|
const id = (tc && toolIds.get(tc)) || str(data.tid) || mintToolId();
|
|
450
553
|
this.lastCompletedTool.set(streamId, id);
|
|
451
554
|
const redacted = deps.redactEnabled();
|
|
452
555
|
put('tool_complete', publicToolFrame(withToolId(complete, id), redacted), { redacted });
|
|
556
|
+
// TAL-397: the todo tool's result is the session's new list; the settled session derives the same one.
|
|
557
|
+
const todos = data.name === 'todo' ? parseTodoToolResult(todoResult) : null;
|
|
558
|
+
if (todos) {
|
|
559
|
+
todos.ts = deps.now();
|
|
560
|
+
this.liveTodos.set(streamId, todos);
|
|
561
|
+
put('todo_state', redactNestedMessageContainers({ ...todos, session_id: sessionId, stream_id: streamId, source: 'live' }, redacted));
|
|
562
|
+
}
|
|
453
563
|
return;
|
|
454
564
|
}
|
|
455
565
|
// Python: the live chat frame carries the queue head plus depth, not the entry that just arrived.
|
|
@@ -490,11 +600,18 @@ export class TurnRunner {
|
|
|
490
600
|
},
|
|
491
601
|
});
|
|
492
602
|
settledAt.value = true;
|
|
603
|
+
// TAL-545: every transcript the Agent hands back settles with the history's images, not the request's projection.
|
|
604
|
+
const withImages = (rows) => withNativeImagesRestored(rows, conversationHistory, apiHistory);
|
|
605
|
+
result.messages = withImages(result.messages);
|
|
606
|
+
if (result.context_messages)
|
|
607
|
+
result.context_messages = withImages(result.context_messages);
|
|
493
608
|
if (this.registry.cancelled.has(streamId) || result.status === 'cancelled') {
|
|
494
609
|
// The Stop's pre-interrupt snapshot is the boundary, even when its reply lands after this result (the sidecar
|
|
495
610
|
// answers each request on its own thread); without one, the Agent's interrupted result is its canonical transcript.
|
|
496
611
|
const interrupted = await this.interrupts.get(streamId);
|
|
497
|
-
|
|
612
|
+
applyAgentUsage(liveUsage, result.usage);
|
|
613
|
+
this.liveUsage.set(streamId, liveUsage);
|
|
614
|
+
this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ? withImages(interrupted.checkpoint) : result.messages);
|
|
498
615
|
put('cancel', this.cancelFrame(sessionId));
|
|
499
616
|
return;
|
|
500
617
|
}
|
|
@@ -513,7 +630,10 @@ export class TurnRunner {
|
|
|
513
630
|
s = current;
|
|
514
631
|
// Python `_maybe_inject_max_iteration_summary_fallback`: an exhausted tool budget leaves the closing
|
|
515
632
|
// explanation in `final_response` only, so it becomes the turn's assistant answer before anything else reads it.
|
|
516
|
-
const
|
|
633
|
+
const withFallback = (rows) => (result.tool_limit_reached ? injectMaxIterationSummaryFallback(rows, result.final_response) : rows);
|
|
634
|
+
const withoutRequest = (rows) => (result.tool_limit_reached ? withoutMaxIterationSummaryRequest(rows, result.max_iterations_summary_request, msgText) : rows);
|
|
635
|
+
const agentRows = withFallback(result.messages);
|
|
636
|
+
const resultMessages = withoutRequest(agentRows);
|
|
517
637
|
// Python `_assistant_reply_added_after_current_turn`: replayed history never counts as this turn's answer (the
|
|
518
638
|
// sidecar reports `completed` whenever a failed run still carries messages).
|
|
519
639
|
// Python's second chance: a turn that emitted no new row still counts when the merged transcript it produced
|
|
@@ -534,31 +654,31 @@ export class TurnRunner {
|
|
|
534
654
|
await this.steerRewrites.get(streamId);
|
|
535
655
|
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, str(result.pending_steer), 'followup');
|
|
536
656
|
followUp = leftovers;
|
|
537
|
-
this.persistError(s, streamId, classification.label, payload, activeTurnToken);
|
|
657
|
+
this.persistError(s, streamId, classification.label, payload, activeTurnToken, resultMessages, result.provider);
|
|
538
658
|
// TAL-512: a btw error carries no session: its journaled frame must not keep a copy of the parent conversation.
|
|
539
659
|
if (!opts.ephemeral)
|
|
540
|
-
payload.session =
|
|
660
|
+
payload.session = this.publicTerminalSession(s);
|
|
541
661
|
payload.session_id = s.session_id;
|
|
542
662
|
payload.old_session_id = sessionId;
|
|
543
663
|
for (const [event, data] of steerEvents)
|
|
544
664
|
put(event, data);
|
|
545
665
|
put('apperror', payload);
|
|
546
|
-
failed = true;
|
|
547
666
|
return;
|
|
548
667
|
}
|
|
549
668
|
// TAL-512: a btw turn passes the failure check above first, so its last assistant row is this turn's reply.
|
|
550
669
|
if (opts.ephemeral) {
|
|
551
670
|
let answer = '';
|
|
552
|
-
for (let i =
|
|
553
|
-
const m =
|
|
671
|
+
for (let i = resultMessages.length - 1; i >= 0; i -= 1) {
|
|
672
|
+
const m = resultMessages[i];
|
|
554
673
|
if (m.role === 'assistant') {
|
|
555
674
|
answer = str(m.content);
|
|
556
675
|
break;
|
|
557
676
|
}
|
|
558
677
|
}
|
|
678
|
+
succeeded = true;
|
|
559
679
|
opts.onDone?.(answer);
|
|
560
680
|
// Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
|
|
561
|
-
put('done', { session: { session_id: sessionId, messages:
|
|
681
|
+
put('done', { session: { session_id: sessionId, messages: resultMessages.map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
|
|
562
682
|
try {
|
|
563
683
|
rmSync(deps.store.pathFor(sessionId), { force: true });
|
|
564
684
|
}
|
|
@@ -567,14 +687,36 @@ export class TurnRunner {
|
|
|
567
687
|
return;
|
|
568
688
|
}
|
|
569
689
|
// ── settle the transcript ──
|
|
690
|
+
// TAL-539: after a mid-turn compression the model context is the sidecar's pruned copy, settled like the transcript.
|
|
691
|
+
const pruned = result.compressed ? result.context_messages ?? null : null;
|
|
692
|
+
const context = withoutToolImages(pruned ? withoutRequest(withFallback(pruned)) : resultMessages);
|
|
693
|
+
let estimate = result.compressed ? result.post_compression_context_tokens_estimate ?? null : null;
|
|
694
|
+
// The sidecar estimated its own rows; a tool-limit settlement or stripped screenshots (TAL-544) that changed them move
|
|
695
|
+
// the estimate by the difference.
|
|
696
|
+
if (pruned && estimate !== null && context !== pruned) {
|
|
697
|
+
try {
|
|
698
|
+
const [before, after] = await Promise.all([pruned, context].map((messages) => sidecar.call('models.estimate_tokens', { messages })));
|
|
699
|
+
estimate = Math.max(1, estimate + after.tokens - before.tokens);
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
estimate = null;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
570
705
|
// The Agent's last pending-steer text settles the remaining steers before the turn is written back.
|
|
571
706
|
await this.steerRewrites.get(streamId);
|
|
572
707
|
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 ?? [] });
|
|
574
|
-
s.context_messages = dedupeContext(
|
|
708
|
+
s.messages = withoutToolImages(mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] }));
|
|
709
|
+
s.context_messages = dedupeContext(context);
|
|
575
710
|
if (result.compressed) {
|
|
576
|
-
s.
|
|
577
|
-
|
|
711
|
+
s.post_compression_context_tokens_estimate = estimate;
|
|
712
|
+
// TAL-540: the anchor is the last visible row before this turn (never its eager checkpoint); the merged transcript
|
|
713
|
+
// holds no compaction marker, so the summary is the model context's newest one.
|
|
714
|
+
const before = visibleMessagesForAnchor(previousMessages.filter((m) => m._turn_id !== streamId));
|
|
715
|
+
const after = visibleMessagesForAnchor(s.messages);
|
|
716
|
+
const anchorIdx = before.length ? before.length - 1 : after.length ? 0 : null;
|
|
717
|
+
s.compression_anchor_visible_idx = anchorIdx;
|
|
718
|
+
s.compression_anchor_message_key = anchorMessageKey(anchorIdx !== null && anchorIdx < after.length ? after[anchorIdx] : after.at(-1));
|
|
719
|
+
s.compression_anchor_summary = markerSummary(s.context_messages);
|
|
578
720
|
}
|
|
579
721
|
const now = deps.now();
|
|
580
722
|
for (const m of s.messages)
|
|
@@ -584,28 +726,29 @@ export class TurnRunner {
|
|
|
584
726
|
const usage = result.usage;
|
|
585
727
|
const prevInputTokens = s.input_tokens || 0;
|
|
586
728
|
const prevCacheReadTokens = s.cache_read_tokens || 0;
|
|
587
|
-
|
|
588
|
-
s.input_tokens = usage.prompt_tokens;
|
|
589
|
-
if (usage.completion_tokens > 0)
|
|
590
|
-
s.output_tokens = usage.completion_tokens;
|
|
591
|
-
if (usage.estimated_cost_usd !== null)
|
|
592
|
-
s.estimated_cost = usage.estimated_cost_usd;
|
|
593
|
-
if (usage.cache_read_tokens > 0)
|
|
594
|
-
s.cache_read_tokens = usage.cache_read_tokens;
|
|
595
|
-
if (usage.cache_write_tokens > 0)
|
|
596
|
-
s.cache_write_tokens = usage.cache_write_tokens;
|
|
729
|
+
applyAgentUsage(s, usage);
|
|
597
730
|
if (typeof result.context.context_length === 'number')
|
|
598
731
|
s.context_length = result.context.context_length;
|
|
599
732
|
if (typeof result.context.threshold_tokens === 'number')
|
|
600
733
|
s.threshold_tokens = result.context.threshold_tokens;
|
|
601
734
|
if (typeof result.context.last_prompt_tokens === 'number')
|
|
602
735
|
s.last_prompt_tokens = result.context.last_prompt_tokens;
|
|
736
|
+
if (result.compressed) {
|
|
737
|
+
// TAL-540, Python `_live_usage_snapshot`: the counters and the ring's post-compression figures, so clients update before `done`.
|
|
738
|
+
const usage = {
|
|
739
|
+
...meteringUsage(s), context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
740
|
+
post_compression_context_tokens_estimate: s.post_compression_context_tokens_estimate, ...contextUsage(s, () => deps.service().deps.contextLengthFor(s.model, s.model_provider)),
|
|
741
|
+
};
|
|
742
|
+
put('compressed', { session_id: sessionId, old_session_id: sessionId, new_session_id: sessionId, continuation_session_id: sessionId, message: 'Compression finished', usage });
|
|
743
|
+
}
|
|
603
744
|
s.tool_calls = extractToolCallsFromMessages(s.messages, liveToolCalls, s.tool_calls);
|
|
604
745
|
s.active_stream_id = null;
|
|
605
746
|
s.pending_user_message = null;
|
|
606
747
|
s.pending_attachments = [];
|
|
607
748
|
s.pending_started_at = null;
|
|
608
749
|
s.pending_user_source = null;
|
|
750
|
+
// TAL-576: a turn that ran means the provider has usable credentials again.
|
|
751
|
+
s.process_wakeup_pause = null;
|
|
609
752
|
const attachments = opts.attachments ?? [];
|
|
610
753
|
if (attachments.length) {
|
|
611
754
|
for (let i = s.messages.length - 1; i >= 0; i -= 1) {
|
|
@@ -656,6 +799,9 @@ export class TurnRunner {
|
|
|
656
799
|
}
|
|
657
800
|
}
|
|
658
801
|
this.persistConsumedSteers(s, streamId, previousStartedAt(s, activeRun), now);
|
|
802
|
+
// TAL-493: the rows this turn's Agent wrote to state.db are now in the transcript, so the merge must not replay them
|
|
803
|
+
// (all of them, the dropped summary request too).
|
|
804
|
+
deps.service().settleStateDb(s, { turnId: streamId, previousContext: this.stopContexts.get(streamId)?.previousContext ?? previousContext, agentRows, startId: this.stopContexts.get(streamId)?.stateDbStartId ?? null });
|
|
659
805
|
deps.store.save(s);
|
|
660
806
|
deps.pending.clearApprovals(sessionId);
|
|
661
807
|
deps.pending.clearClarifies(sessionId);
|
|
@@ -682,7 +828,7 @@ export class TurnRunner {
|
|
|
682
828
|
cache_hit_percent: cacheHit(cacheReadTokens, inputTokens), turn_cache_hit_percent: cacheHit(turnCacheRead, turnInput), duration_seconds: Math.round(duration * 1000) / 1000,
|
|
683
829
|
context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
684
830
|
};
|
|
685
|
-
const doneSession =
|
|
831
|
+
const doneSession = this.publicTerminalSession(s);
|
|
686
832
|
// TAL-299: the ring's figures match the terminal session's (and a detail reload's).
|
|
687
833
|
for (const key of CONTEXT_USAGE_FIELDS)
|
|
688
834
|
doneUsage[key] = doneSession[key];
|
|
@@ -700,6 +846,8 @@ export class TurnRunner {
|
|
|
700
846
|
};
|
|
701
847
|
if (result.tool_limit_reached)
|
|
702
848
|
donePayload.terminal_reason = 'max_iterations';
|
|
849
|
+
// TAL-397: the live meter ends on the persisted counters and turn rate.
|
|
850
|
+
put('metering', { session_id: sessionId, usage: meteringUsage(s), tps: doneUsage.tps ?? null, tps_available: doneUsage.tps !== undefined, estimated: false });
|
|
703
851
|
put('done', donePayload);
|
|
704
852
|
for (const [event, payload] of steerEvents)
|
|
705
853
|
put(event, payload);
|
|
@@ -709,20 +857,21 @@ export class TurnRunner {
|
|
|
709
857
|
this.registry.activeRuns.delete(streamId);
|
|
710
858
|
this.settledStreams.add(streamId);
|
|
711
859
|
this.startSteerFollowUp(sessionId, leftovers);
|
|
860
|
+
// Before the title work, so a generated title's own state.db sync lands after this one's provisional title.
|
|
861
|
+
await deps.syncUsage?.(s, usedModel || null);
|
|
712
862
|
// Title work and the goal judge run side by side; `stream_end` follows both, so their frames reach the stream.
|
|
713
863
|
const work = await Promise.allSettled([this.backgroundTitle(s, put), this.continueGoal(s, streamId, put)]);
|
|
714
864
|
put('stream_end', { session_id: sessionId });
|
|
715
865
|
for (const r of work)
|
|
716
866
|
if (r.status === 'rejected')
|
|
717
867
|
throw r.reason;
|
|
718
|
-
|
|
719
|
-
opts.onDone?.(lastAnswer(s.messages)
|
|
868
|
+
succeeded = true;
|
|
869
|
+
opts.onDone?.(lastAnswer(s.messages));
|
|
720
870
|
}
|
|
721
871
|
catch (error) {
|
|
722
872
|
if (settledAt.value && !(error instanceof SidecarError)) {
|
|
723
873
|
deps.log(`[webui] ERROR settling turn ${streamId}\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
724
874
|
}
|
|
725
|
-
failed = true;
|
|
726
875
|
if (this.registry.cancelled.has(streamId)) {
|
|
727
876
|
this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
728
877
|
put('cancel', this.cancelFrame(sessionId));
|
|
@@ -749,7 +898,7 @@ export class TurnRunner {
|
|
|
749
898
|
put(event, data);
|
|
750
899
|
this.persistError(current, streamId, classification.label, payload, activeTurnToken);
|
|
751
900
|
if (!opts.ephemeral)
|
|
752
|
-
payload.session =
|
|
901
|
+
payload.session = this.publicTerminalSession(current);
|
|
753
902
|
}
|
|
754
903
|
}
|
|
755
904
|
catch (persistError) {
|
|
@@ -763,51 +912,54 @@ export class TurnRunner {
|
|
|
763
912
|
this.teardown(sessionId, streamId);
|
|
764
913
|
// After persistence and teardown: a background route's failure cleanup deletes the hidden session, which must
|
|
765
914
|
// not race the error writeback above (a re-saved session would resurface in the sidebar).
|
|
766
|
-
if (
|
|
767
|
-
opts.onFailed?.();
|
|
915
|
+
if (!succeeded)
|
|
916
|
+
opts.onFailed?.(failure);
|
|
768
917
|
this.startSteerFollowUp(sessionId, followUp);
|
|
769
918
|
}
|
|
770
919
|
}
|
|
771
920
|
/**
|
|
772
|
-
* Python `
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
* and its transcript row a distinct identity.
|
|
921
|
+
* Python `_resolve_image_input_mode`: the Agent's image mode for this turn's model under its profile config, or null
|
|
922
|
+
* when it is unknown (no sidecar, a cancel, a failed lookup). A canonical "text" is honoured only with an explicit
|
|
923
|
+
* text signal or a model KNOWN to be text-only; an unknown/custom model forwards natively and lets the Agent's retry
|
|
924
|
+
* guard downgrade.
|
|
777
925
|
*/
|
|
778
|
-
async
|
|
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);
|
|
782
|
-
const candidates = attachments.filter((att) => str(att.path).trim() && str(att.mime).trim().startsWith('image/'));
|
|
783
|
-
if (!candidates.length)
|
|
784
|
-
return withFiles();
|
|
926
|
+
async imageInputMode(sessionId, s, opts, signal) {
|
|
785
927
|
const sidecar = this.deps.sidecar();
|
|
786
|
-
if (!sidecar)
|
|
787
|
-
return withFiles();
|
|
788
928
|
// A cancel that landed before this point is final: never start the lookup or wait on it.
|
|
789
|
-
if (signal.aborted)
|
|
790
|
-
return
|
|
929
|
+
if (!sidecar || signal.aborted)
|
|
930
|
+
return null;
|
|
931
|
+
// The session's provider before the Agent canonicalizes it, so a `custom:<name>` override entry still matches.
|
|
932
|
+
const provider = str(opts.modelProvider ?? s.model_provider);
|
|
791
933
|
try {
|
|
792
934
|
// A cancelled turn must not sit behind this lookup: the abort wins the race and `run()` then takes the cancelled path.
|
|
793
|
-
const lookup = sidecar.call('text.image_mode', { profile_home: this.deps.profileHome(s.profile), provider
|
|
935
|
+
const lookup = sidecar.call('text.image_mode', { profile_home: this.deps.profileHome(s.profile), provider, model: str(opts.model ?? s.model), requested_provider: provider }, { signal, timeoutMs: IMAGE_MODE_TIMEOUT_MS });
|
|
794
936
|
const mode = await Promise.race([lookup, new Promise((_, reject) => { if (signal.aborted) {
|
|
795
937
|
reject(new Error('turn cancelled'));
|
|
796
938
|
return;
|
|
797
939
|
} signal.addEventListener('abort', () => { reject(new Error('turn cancelled')); }, { once: true }); })]);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
if (explicitTextSignal(cfg) || mode.supports_vision === false)
|
|
803
|
-
return withFiles();
|
|
804
|
-
}
|
|
940
|
+
if (mode.mode === 'native')
|
|
941
|
+
return 'native';
|
|
942
|
+
const cfg = (await this.deps.profileConfig?.(s.profile ?? null)) ?? {};
|
|
943
|
+
return explicitTextSignal(cfg) || mode.supports_vision === false ? 'text' : 'native';
|
|
805
944
|
}
|
|
806
945
|
catch (error) {
|
|
807
946
|
if (!signal.aborted)
|
|
808
947
|
this.deps.log(`[webui] image mode lookup failed for ${sessionId}: ${error.message}`);
|
|
809
|
-
return
|
|
948
|
+
return null;
|
|
810
949
|
}
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Python `_build_user_message`: image attachments are embedded as native `image_url` parts only when the turn's
|
|
953
|
+
* image mode is `native` (text mode routes them through the Agent's vision tool path), and only after the bytes are
|
|
954
|
+
* read through an anchored descriptor and sniffed as a real image format. Every attached file is also named by path
|
|
955
|
+
* after the text (TAL-276), so an attachment-only turn still gives the model a request and its transcript row a
|
|
956
|
+
* distinct identity.
|
|
957
|
+
*/
|
|
958
|
+
buildUserMessage(msgText, attachments, workspace, sessionId, imageMode) {
|
|
959
|
+
const withFiles = () => attachedFilesPrompt(msgText, attachments);
|
|
960
|
+
const candidates = attachments.filter(isImageAttachment);
|
|
961
|
+
if (!candidates.length || imageMode !== 'native')
|
|
962
|
+
return withFiles();
|
|
811
963
|
const parts = [];
|
|
812
964
|
let images = 0;
|
|
813
965
|
const roots = [workspace, this.deps.attachmentDir(sessionId)].map((r) => resolvePathLikePython(r));
|
|
@@ -842,6 +994,10 @@ export class TurnRunner {
|
|
|
842
994
|
}
|
|
843
995
|
return images ? [{ type: 'text', text: withFiles() }, ...parts] : withFiles();
|
|
844
996
|
}
|
|
997
|
+
/** The settled session as a terminal frame carries it: the display window, redacted. */
|
|
998
|
+
publicTerminalSession(s) {
|
|
999
|
+
return redactSessionData(this.terminalSessionPayload(s), this.deps.redactEnabled());
|
|
1000
|
+
}
|
|
845
1001
|
/**
|
|
846
1002
|
* Python `_session_payload_with_terminal_window`: the same visible-message window contract as `GET /api/session?
|
|
847
1003
|
* msg_limit=` (renderable rows, limited payload shape, windowed tool calls, todo state) with the full count.
|
|
@@ -858,13 +1014,28 @@ export class TurnRunner {
|
|
|
858
1014
|
payload.message_count = s.messages.length;
|
|
859
1015
|
payload._messages_offset = offset;
|
|
860
1016
|
payload._messages_truncated = offset > 0;
|
|
861
|
-
attachTodoState(payload, s.messages);
|
|
1017
|
+
attachTodoState(payload, s.messages, s.extra[UNSETTLED_TODO_KEY]);
|
|
862
1018
|
payload.tool_calls = toolCallsForMessageWindow(s.tool_calls, offset, limited.length);
|
|
1019
|
+
payload.compression_reference = compressionReference(s, scened);
|
|
863
1020
|
return payload;
|
|
864
1021
|
}
|
|
865
1022
|
/** Python `_materialize_pending_user_turn_before_error` + error message append + save. */
|
|
866
|
-
|
|
1023
|
+
/** `runtimeProvider`: the provider the Agent resolved for the turn, when it reported one. */
|
|
1024
|
+
persistError(s, streamId, label, payload, activeTurnToken, agentRows = [], runtimeProvider = '') {
|
|
867
1025
|
const startedAt = s.pending_started_at;
|
|
1026
|
+
// TAL-576: an automatic wakeup with no usable credentials pauses the next ones instead of failing each.
|
|
1027
|
+
if (s.pending_user_source === 'process_wakeup' && payload.type === 'credential_pool_empty') {
|
|
1028
|
+
// A profile whose home now escapes the profiles root (`profileHome` throws) records no pause; the error still persists.
|
|
1029
|
+
let home = null;
|
|
1030
|
+
try {
|
|
1031
|
+
home = this.deps.profileHome(s.profile);
|
|
1032
|
+
}
|
|
1033
|
+
catch {
|
|
1034
|
+
home = null;
|
|
1035
|
+
}
|
|
1036
|
+
if (home !== null)
|
|
1037
|
+
recordWakeupPause(s, home, this.deps.now(), runtimeProvider, this.deps.configPath?.(home));
|
|
1038
|
+
}
|
|
868
1039
|
this.materializePendingUserTurn(s, activeTurnToken, streamId);
|
|
869
1040
|
const duration = typeof startedAt === 'number' && startedAt > 0 ? Math.max(0, this.deps.now() - startedAt) : null;
|
|
870
1041
|
s.active_stream_id = null;
|
|
@@ -893,17 +1064,40 @@ export class TurnRunner {
|
|
|
893
1064
|
else if (payload.type === 'interrupted')
|
|
894
1065
|
errorMessage.provider_details_label = 'Interruption details';
|
|
895
1066
|
s.messages.push(errorMessage);
|
|
1067
|
+
this.keepLiveState(s, streamId);
|
|
896
1068
|
this.persistConsumedSteers(s, streamId, startedAt, this.deps.now());
|
|
1069
|
+
// TAL-921: the next prompt remembers the turn's work; an exhausted compression's work would only overflow again. One
|
|
1070
|
+
// read builds the context and settles, so a row committed in between is never covered without reaching either.
|
|
1071
|
+
const read = this.deps.service().stateDbRead(s);
|
|
1072
|
+
const context = payload.type === 'compression_exhausted' ? null : this.unsettledContext(streamId, agentRows, read);
|
|
1073
|
+
if (context)
|
|
1074
|
+
s.context_messages = context;
|
|
1075
|
+
const stop = this.stopContexts.get(streamId);
|
|
1076
|
+
this.deps.service().settleStateDb(s, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows, startId: stop?.stateDbStartId ?? null, read });
|
|
897
1077
|
try {
|
|
898
1078
|
this.deps.store.save(s);
|
|
899
1079
|
}
|
|
900
1080
|
catch (error) {
|
|
901
1081
|
this.deps.log(`[webui] WARNING: failed to save error turn for ${s.session_id}: ${error.message}`);
|
|
902
1082
|
}
|
|
1083
|
+
// The kept live counters are the session's usage now, so the insights row follows them (the runtime logs failures).
|
|
1084
|
+
void this.deps.syncUsage?.(s, str(s.model) || null);
|
|
903
1085
|
this.deps.pending.clearApprovals(s.session_id);
|
|
904
1086
|
this.deps.pending.clearClarifies(s.session_id);
|
|
905
1087
|
this.deps.events.publish('session_error', { profile: s.profile, sessionId: s.session_id });
|
|
906
1088
|
}
|
|
1089
|
+
/**
|
|
1090
|
+
* TAL-921: the model context a turn that ends without its settled result leaves: the work its Agent reported, else the
|
|
1091
|
+
* rows it committed past its starting read in `read`, the read its settlement uses.
|
|
1092
|
+
*/
|
|
1093
|
+
unsettledContext(streamId, agentRows, read) {
|
|
1094
|
+
const stop = this.stopContexts.get(streamId);
|
|
1095
|
+
if (!stop)
|
|
1096
|
+
return null;
|
|
1097
|
+
const after = stop.stateDbStartId;
|
|
1098
|
+
const stateRows = after === null ? [] : read.rows.filter((m) => typeof m._state_db_row_id === 'number' && m._state_db_row_id > after);
|
|
1099
|
+
return this.deps.service().unsettledTurnContext({ ...stop, streamedText: (this.registry.partialText.get(streamId) ?? []).join(''), agentRows, stateRows });
|
|
1100
|
+
}
|
|
907
1101
|
materializePendingUserTurn(s, activeTurnToken, turnId) {
|
|
908
1102
|
const pendingText = str(s.pending_user_message);
|
|
909
1103
|
if (!s.hasPendingPrompt)
|
|
@@ -953,7 +1147,7 @@ export class TurnRunner {
|
|
|
953
1147
|
let snapshot = null;
|
|
954
1148
|
if (sessionId)
|
|
955
1149
|
try {
|
|
956
|
-
snapshot =
|
|
1150
|
+
snapshot = this.publicTerminalSession(this.deps.store.get(sessionId));
|
|
957
1151
|
}
|
|
958
1152
|
catch {
|
|
959
1153
|
snapshot = null;
|
|
@@ -988,24 +1182,41 @@ export class TurnRunner {
|
|
|
988
1182
|
// TAL-460: a background turn stops quietly: its prompt and model context are kept, but no partial and no Stop row,
|
|
989
1183
|
// and its first settlement is final.
|
|
990
1184
|
const quiet = this.registry.activeRuns.get(streamId)?.origin === 'background';
|
|
991
|
-
if (quiet && current.active_stream_id === null)
|
|
1185
|
+
if (quiet && current.active_stream_id === null) {
|
|
1186
|
+
// TAL-493: rows its worker committed while unwinding are covered by what it reports; others are kept.
|
|
1187
|
+
if (checkpoint) {
|
|
1188
|
+
this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: this.stopContexts.get(streamId)?.previousContext ?? [], agentRows: checkpoint, startId: this.stopContexts.get(streamId)?.stateDbStartId ?? null });
|
|
1189
|
+
try {
|
|
1190
|
+
this.deps.store.save(current);
|
|
1191
|
+
}
|
|
1192
|
+
catch {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
992
1196
|
return true;
|
|
1197
|
+
}
|
|
993
1198
|
const stop = this.stopContexts.get(streamId);
|
|
994
1199
|
const canonical = stop !== undefined && checkpoint !== null && checkpointTurnStart(checkpoint, stop.msgText, stop.historyLength) !== null;
|
|
995
|
-
|
|
1200
|
+
// TAL-921: one state.db read builds the context and settles the turn.
|
|
1201
|
+
const settle = (read) => {
|
|
996
1202
|
if (!stop)
|
|
997
1203
|
return null;
|
|
998
1204
|
if (canonical)
|
|
999
1205
|
stop.checkpointed = true;
|
|
1000
|
-
return
|
|
1206
|
+
return this.unsettledContext(streamId, checkpoint, read);
|
|
1001
1207
|
};
|
|
1002
1208
|
if (current.messages.some((m) => isCancelMarker(m)) && current.active_stream_id === null && !current.pending_user_message) {
|
|
1003
1209
|
// The worker's canonical result can arrive after cancel() settled without a checkpoint (the interrupt reply failed
|
|
1004
1210
|
// or timed out): it replaces this stream's context and never adds a second Stop row. A settlement that already had
|
|
1005
1211
|
// one keeps it, so work the Agent finished after the Stop stays out.
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
|
|
1212
|
+
const ownStop = current.messages.some((m) => isCancelMarker(m) && m._turn_id === streamId);
|
|
1213
|
+
// TAL-493: the worker's rows committed after the first settlement are covered by what it reports; others are kept.
|
|
1214
|
+
if (ownStop && checkpoint) {
|
|
1215
|
+
const read = this.deps.service().stateDbRead(current);
|
|
1216
|
+
const late = canonical && !stop.checkpointed ? settle(read) : null;
|
|
1217
|
+
if (late)
|
|
1218
|
+
current.context_messages = dedupeContext(late);
|
|
1219
|
+
this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows: checkpoint, startId: stop?.stateDbStartId ?? null, read });
|
|
1009
1220
|
try {
|
|
1010
1221
|
this.deps.store.save(current);
|
|
1011
1222
|
}
|
|
@@ -1017,7 +1228,8 @@ export class TurnRunner {
|
|
|
1017
1228
|
}
|
|
1018
1229
|
const startedAt = current.pending_started_at;
|
|
1019
1230
|
this.materializePendingUserTurn(current, buildActiveTurnToken(streamId, current.pending_started_at), streamId);
|
|
1020
|
-
const
|
|
1231
|
+
const read = this.deps.service().stateDbRead(current);
|
|
1232
|
+
const context = settle(read);
|
|
1021
1233
|
if (context)
|
|
1022
1234
|
current.context_messages = dedupeContext(context);
|
|
1023
1235
|
current.active_stream_id = null;
|
|
@@ -1029,13 +1241,16 @@ export class TurnRunner {
|
|
|
1029
1241
|
this.appendPartialSnapshot(current, streamId);
|
|
1030
1242
|
current.messages.push({ role: 'assistant', content: '', _error: true, _terminal_state: 'cancelled', timestamp: Math.trunc(this.deps.now()), _turn_id: streamId });
|
|
1031
1243
|
}
|
|
1244
|
+
this.keepLiveState(current, streamId);
|
|
1032
1245
|
this.persistConsumedSteers(current, streamId, startedAt, this.deps.now());
|
|
1246
|
+
this.deps.service().settleStateDb(current, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows: checkpoint, startId: stop?.stateDbStartId ?? null, read });
|
|
1033
1247
|
try {
|
|
1034
1248
|
this.deps.store.save(current);
|
|
1035
1249
|
}
|
|
1036
1250
|
catch {
|
|
1037
1251
|
return false;
|
|
1038
1252
|
}
|
|
1253
|
+
void this.deps.syncUsage?.(current, str(current.model) || null);
|
|
1039
1254
|
this.deps.pending.clearApprovals(current.session_id);
|
|
1040
1255
|
this.deps.pending.clearClarifies(current.session_id);
|
|
1041
1256
|
this.deps.events.publish('session_cancel', { profile: current.profile, sessionId: current.session_id });
|
|
@@ -1045,7 +1260,18 @@ export class TurnRunner {
|
|
|
1045
1260
|
emitToSession(sessionId, event, data) {
|
|
1046
1261
|
this.sessionPuts.get(sessionId)?.(event, data);
|
|
1047
1262
|
}
|
|
1263
|
+
/** TAL-397: a turn that ends without its result keeps what the live stream already showed: counters and todo list. */
|
|
1264
|
+
keepLiveState(s, streamId) {
|
|
1265
|
+
const usage = this.liveUsage.get(streamId);
|
|
1266
|
+
if (usage)
|
|
1267
|
+
Object.assign(s, usage);
|
|
1268
|
+
const todos = this.liveTodos.get(streamId);
|
|
1269
|
+
if (todos)
|
|
1270
|
+
s.extra[UNSETTLED_TODO_KEY] = todos;
|
|
1271
|
+
}
|
|
1048
1272
|
teardown(sessionId, streamId) {
|
|
1273
|
+
this.liveUsage.delete(streamId);
|
|
1274
|
+
this.liveTodos.delete(streamId);
|
|
1049
1275
|
const writer = this.writers.get(streamId);
|
|
1050
1276
|
if (writer) {
|
|
1051
1277
|
try {
|
|
@@ -1113,13 +1339,14 @@ export class TurnRunner {
|
|
|
1113
1339
|
}
|
|
1114
1340
|
return { title: '', status, rawPreview: '' };
|
|
1115
1341
|
}
|
|
1116
|
-
/**
|
|
1342
|
+
/**
|
|
1343
|
+
* Python `generate_session_title_for_session`: on-demand title from the persisted transcript; never touches
|
|
1344
|
+
* `llm_title_generated`. An explicit regenerate runs even with `auxiliary.title_generation.enabled` off (TAL-531).
|
|
1345
|
+
*/
|
|
1117
1346
|
async generateTitle(s, opts = {}) {
|
|
1118
1347
|
const [userText, assistantText] = opts.preferLatest ? latestExchangeSnippets(s.messages) : firstExchangeSnippets(s.messages, { scanPastConsecutiveUsers: true });
|
|
1119
1348
|
if (!userText)
|
|
1120
1349
|
return { title: null, status: 'empty_user_message', rawPreview: '' };
|
|
1121
|
-
if (!this.deps.titleGenerationEnabled())
|
|
1122
|
-
return { title: null, status: 'title_generation_disabled', rawPreview: '' };
|
|
1123
1350
|
let next = '';
|
|
1124
1351
|
let llmStatus = 'empty_assistant_message';
|
|
1125
1352
|
let rawPreview = '';
|
|
@@ -1132,6 +1359,15 @@ export class TurnRunner {
|
|
|
1132
1359
|
return { title: fallback, status: `local_summary:${llmStatus}`, rawPreview };
|
|
1133
1360
|
return { title: null, status: llmStatus || 'empty_title', rawPreview };
|
|
1134
1361
|
}
|
|
1362
|
+
/**
|
|
1363
|
+
* TAL-591 (Python `_queue_generated_title_for_imported_session`): a CLI session a mutation claims while it still
|
|
1364
|
+
* carries its default title gets the background title job at once instead of after its first WebUI turn.
|
|
1365
|
+
*/
|
|
1366
|
+
async titleClaimed(s) {
|
|
1367
|
+
if (!looksLikeDefaultCliTitle({ title: s.title, source_tag: s.source_tag, raw_source: s.raw_source, session_source: s.session_source, source_label: s.source_label }))
|
|
1368
|
+
return;
|
|
1369
|
+
await this.backgroundTitle(s, () => undefined).catch((error) => { this.deps.log(`[webui] imported session title failed for ${s.session_id}: ${error.message}`); });
|
|
1370
|
+
}
|
|
1135
1371
|
/** Python `_run_background_title_update` (spawned only when `_background_title_generation_inputs` is eligible). */
|
|
1136
1372
|
async backgroundTitle(s, put) {
|
|
1137
1373
|
const sessionId = s.session_id;
|
|
@@ -1145,13 +1381,22 @@ export class TurnRunner {
|
|
|
1145
1381
|
const status = (status, reason = '', title = '', rawPreview = '') => {
|
|
1146
1382
|
put('title_status', { session_id: sessionId, status, ...(reason ? { reason } : {}), ...(title ? { title } : {}), ...(rawPreview ? { raw_preview: rawPreview } : {}) });
|
|
1147
1383
|
};
|
|
1148
|
-
|
|
1384
|
+
// Python ran the adaptive refresh only when the first-title job was not eligible.
|
|
1385
|
+
if (!eligible || (s.llm_title_generated && !invalidExisting) || !userText || !assistantText) {
|
|
1386
|
+
await this.refreshTitle(s, put, status);
|
|
1149
1387
|
return;
|
|
1388
|
+
}
|
|
1150
1389
|
if (s.manual_title) {
|
|
1151
1390
|
status('skipped', 'manual_title', placeholder);
|
|
1152
1391
|
return;
|
|
1153
1392
|
}
|
|
1154
|
-
|
|
1393
|
+
// A missing config.yaml reads as `{}` (enabled); an unreadable one cannot confirm the user has not opted out.
|
|
1394
|
+
const cfg = await this.deps.profileConfig?.(s.profile ?? null);
|
|
1395
|
+
if (!cfg) {
|
|
1396
|
+
status('skipped', 'config_unavailable', placeholder);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (!titleGenerationEnabled(cfg)) {
|
|
1155
1400
|
status('skipped', 'title_generation_disabled', placeholder);
|
|
1156
1401
|
return;
|
|
1157
1402
|
}
|
|
@@ -1172,6 +1417,9 @@ export class TurnRunner {
|
|
|
1172
1417
|
current = this.deps.store.get(sessionId);
|
|
1173
1418
|
}
|
|
1174
1419
|
catch {
|
|
1420
|
+
// A session deleted while the title prompt ran stays deleted.
|
|
1421
|
+
if (this.deps.store.wasDeleted(sessionId))
|
|
1422
|
+
return;
|
|
1175
1423
|
current = s;
|
|
1176
1424
|
}
|
|
1177
1425
|
let effective = str(current.title).trim();
|
|
@@ -1200,6 +1448,71 @@ export class TurnRunner {
|
|
|
1200
1448
|
else
|
|
1201
1449
|
status('skipped', source || 'unchanged', effective, rawPreview);
|
|
1202
1450
|
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Python `_maybe_schedule_title_refresh` + `_run_background_title_refresh`: every N exchanges a generated title is
|
|
1453
|
+
* regenerated from the latest exchange; a manual title is never touched.
|
|
1454
|
+
*/
|
|
1455
|
+
async refreshTitle(s, put, status) {
|
|
1456
|
+
const every = this.deps.titleRefreshEvery?.() ?? 0;
|
|
1457
|
+
const exchanges = countExchanges(s.messages);
|
|
1458
|
+
if (every <= 0 || exchanges <= 0 || exchanges % every !== 0)
|
|
1459
|
+
return;
|
|
1460
|
+
const [userText, assistantText] = latestExchangeSnippets(s.messages);
|
|
1461
|
+
if (!userText || !assistantText)
|
|
1462
|
+
return;
|
|
1463
|
+
// Admission is released before title work, so an earlier turn's refresh may still be running: refreshes apply in
|
|
1464
|
+
// turn order and each reads the title its predecessor wrote.
|
|
1465
|
+
const sessionId = s.session_id;
|
|
1466
|
+
const run = (this.titleRefreshes.get(sessionId) ?? Promise.resolve()).catch(() => undefined).then(() => this.applyTitleRefresh(sessionId, s, userText, assistantText, put, status));
|
|
1467
|
+
this.titleRefreshes.set(sessionId, run);
|
|
1468
|
+
try {
|
|
1469
|
+
await run;
|
|
1470
|
+
}
|
|
1471
|
+
finally {
|
|
1472
|
+
if (this.titleRefreshes.get(sessionId) === run)
|
|
1473
|
+
this.titleRefreshes.delete(sessionId);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
async applyTitleRefresh(sessionId, s, userText, assistantText, put, status) {
|
|
1477
|
+
const read = () => { try {
|
|
1478
|
+
return this.deps.store.get(sessionId);
|
|
1479
|
+
}
|
|
1480
|
+
catch {
|
|
1481
|
+
return s;
|
|
1482
|
+
} };
|
|
1483
|
+
const latest = read();
|
|
1484
|
+
const placeholder = str(latest.title).trim();
|
|
1485
|
+
if (['Untitled', 'New Chat', ''].includes(placeholder) || latest.manual_title || !latest.llm_title_generated)
|
|
1486
|
+
return;
|
|
1487
|
+
const cfg = await this.deps.profileConfig?.(latest.profile ?? null);
|
|
1488
|
+
if (!cfg || !titleGenerationEnabled(cfg)) {
|
|
1489
|
+
status('refresh_skipped', cfg ? 'title_generation_disabled' : 'config_unavailable', placeholder);
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
const { title: next, status: llmStatus, rawPreview } = await this.llmTitle(latest, userText, assistantText);
|
|
1493
|
+
if (!next) {
|
|
1494
|
+
status('refresh_skipped', llmStatus || 'empty', placeholder, rawPreview);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const normalize = (value) => value.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1498
|
+
if (normalize(next) === normalize(placeholder)) {
|
|
1499
|
+
status('refresh_skipped', 'same_title', placeholder, rawPreview);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const current = read();
|
|
1503
|
+
// The user may have renamed the session while the title prompt ran.
|
|
1504
|
+
if (current.manual_title || str(current.title).trim() !== placeholder) {
|
|
1505
|
+
status('skipped', 'manual_title', str(current.title).trim());
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
current.title = next;
|
|
1509
|
+
markSessionTitleGenerated(current);
|
|
1510
|
+
this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1511
|
+
await this.deps.syncTitle?.(current);
|
|
1512
|
+
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
1513
|
+
status('refreshed', llmStatus, next, rawPreview);
|
|
1514
|
+
put('title', { session_id: sessionId, title: next });
|
|
1515
|
+
}
|
|
1203
1516
|
// ── cancel / steer ───────────────────────────────────────────────────────
|
|
1204
1517
|
/**
|
|
1205
1518
|
* Python `cancel_stream`: persist the partial, mark cancelled, interrupt the Agent, release admission. `withdrawn` is the
|
|
@@ -1477,12 +1790,6 @@ export class TurnRunner {
|
|
|
1477
1790
|
return { events, leftovers: [...(unmatched ? [unmatched] : []), ...left.map((r) => r.text)] };
|
|
1478
1791
|
if (unmatched)
|
|
1479
1792
|
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() }]);
|
|
1484
|
-
if (unmatched)
|
|
1485
|
-
events.push(['pending_steer_leftover', { stream_id: streamId, text: unmatched }]);
|
|
1486
1793
|
return { events, leftovers: [] };
|
|
1487
1794
|
}
|
|
1488
1795
|
/**
|
|
@@ -1709,7 +2016,9 @@ export class TurnRunner {
|
|
|
1709
2016
|
const profileHome = this.deps.profileHome(s?.profile ?? null);
|
|
1710
2017
|
let resolved = false;
|
|
1711
2018
|
let withdrawn = false;
|
|
1712
|
-
if (entry)
|
|
2019
|
+
if (entry?._injected === true)
|
|
2020
|
+
resolved = Boolean(this.deps.pending.resolveApproval(sessionId, str(entry.approval_id)).entry);
|
|
2021
|
+
else if (entry) {
|
|
1713
2022
|
if (!sidecar)
|
|
1714
2023
|
return relayFailure('not running');
|
|
1715
2024
|
try {
|
|
@@ -1811,6 +2120,11 @@ export class TurnRunner {
|
|
|
1811
2120
|
const response = typeof reply === 'string' ? reply : clarifyReply(entry, reply);
|
|
1812
2121
|
if (response === null)
|
|
1813
2122
|
return { ok: false, invalid: true };
|
|
2123
|
+
// A test-hook prompt has no parked Agent waiter: it is answered here.
|
|
2124
|
+
if (entry._injected === true) {
|
|
2125
|
+
this.deps.pending.resolveClarify(sessionId, str(entry.clarify_id));
|
|
2126
|
+
return { ok: true, response };
|
|
2127
|
+
}
|
|
1814
2128
|
const sidecar = this.deps.sidecar();
|
|
1815
2129
|
if (!sidecar)
|
|
1816
2130
|
return { ok: false, error: 'The Agent sidecar is not running; retry in a moment.' };
|
|
@@ -1848,18 +2162,6 @@ function previousStartedAt(s, run) {
|
|
|
1848
2162
|
function isCancelMarker(m) {
|
|
1849
2163
|
return m._error === true && (m._terminal_state === 'cancelled' || str(m.content).startsWith('**Task cancelled:**'));
|
|
1850
2164
|
}
|
|
1851
|
-
function dedupeContext(messages) {
|
|
1852
|
-
const out = [];
|
|
1853
|
-
let lastKey = null;
|
|
1854
|
-
for (const m of messages) {
|
|
1855
|
-
const key = messageIdentity(m);
|
|
1856
|
-
if (key !== null && key === lastKey && m.role === 'assistant')
|
|
1857
|
-
continue;
|
|
1858
|
-
out.push(m);
|
|
1859
|
-
lastKey = key;
|
|
1860
|
-
}
|
|
1861
|
-
return out;
|
|
1862
|
-
}
|
|
1863
2165
|
export { HttpFailure };
|
|
1864
2166
|
/** Where a consumed steer goes: past the tool results of the call it followed, else past the turn's prompt. */
|
|
1865
2167
|
function roundDuration(seconds) {
|