@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/service.js
CHANGED
|
@@ -3,16 +3,23 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { rmSync } from 'node:fs';
|
|
4
4
|
import { buildActiveTurnToken, copyJson, redactSessionData, redactValue, stripPublicInternalFields, withSceneToolDisplay } from '../redact.js';
|
|
5
5
|
import { DraftVersionConflict, normalizeDraftVersion } from './drafts.js';
|
|
6
|
-
import { allSessions, buildSessionListPayload, isClaimableCliSource, isMessagingSessionRecord, withOwnerLocks, withSessionWireFlags, lineageRootId, mergeCliSidebarMetadata, sessionListResponse, sessionSearchMessageText, sessionSearchPreview } from './list.js';
|
|
7
|
-
import { anchorSceneIntOrNull, hydrateAnchorActivityScenes, normalizeAnchorSceneMessageRef, readAnchorSceneRows, storeAnchorScene, withTurnIds } from './anchor.js';
|
|
8
|
-
import { isSafeSessionId, lastMessageTimestamp, Session, titleFrom } from './session.js';
|
|
6
|
+
import { allSessions, buildSessionListPayload, CLI_IDENTITY_FIELDS, isClaimableCliSource, isMessagingSessionRecord, withOwnerLocks, withSessionWireFlags, lineageRootId, mergeCliSidebarMetadata, sessionListResponse, sessionSearchMessageText, sessionSearchPreview } from './list.js';
|
|
7
|
+
import { anchorSceneIntOrNull, fullToolResult, hydrateAnchorActivityScenes, normalizeAnchorSceneMessageRef, readAnchorSceneRows, storeAnchorScene, withTurnIds } from './anchor.js';
|
|
8
|
+
import { isSafeSessionId, lastMessageTimestamp, Session, stripAttachedFilesMarker, titleFrom } from './session.js';
|
|
9
9
|
import { SessionBusy, SessionNotFound, statSignature } from './store.js';
|
|
10
10
|
import { attachTodoState } from './todo.js';
|
|
11
11
|
import { stateDbSessionMessages, stateDbSessionRow, stateDbSessionSources } from './state-db.js';
|
|
12
|
-
import {
|
|
12
|
+
import { anchorMessageKey, anchorSummary, CompressionJobs, visibleMessagesForAnchor } from './compress.js';
|
|
13
|
+
import { SidecarError } from '../sidecar/client.js';
|
|
14
|
+
import { attachmentObjects, isContextCompressionMarker, mergeSessionMessagesAppendOnly, pendingUserRow, sanitizeMessagesForApi, stripWorkspacePrefix, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withSceneRowMedia, withPendingUserTurn, withToolCallOutcomes, withoutRunningTurnOutput } from './merge.js';
|
|
15
|
+
import { withBackgroundUpdates } from './background-updates.js';
|
|
16
|
+
import { withBackgroundLinks } from './background-tasks.js';
|
|
13
17
|
import { messagesForLimitedPayload, messageWindowForDisplay, MAX_MSG_LIMIT, parseMsgLimit, toolCallsForMessageWindow } from './window.js';
|
|
14
18
|
import { redactText } from '../redact.js';
|
|
19
|
+
import { workspaceDisplayName } from '../workspace/workspaces.js';
|
|
15
20
|
import { buildShareSnapshot } from './shares.js';
|
|
21
|
+
import { mediaAnchorRoot, mediaRefPath, mediaTarget } from '../workspace/media.js';
|
|
22
|
+
import { projectMediaRefs } from '../workspace/media-refs.js';
|
|
16
23
|
import { loadGatewaySessionIdentityMap } from './list.js';
|
|
17
24
|
import { join } from 'node:path';
|
|
18
25
|
export class HttpFailure extends Error {
|
|
@@ -25,13 +32,65 @@ export class HttpFailure extends Error {
|
|
|
25
32
|
this.name = 'HttpFailure';
|
|
26
33
|
}
|
|
27
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Python `_agent_runtime_barrier_response`: a stale local Agent checkout is refused with a typed 409 before the caller
|
|
37
|
+
* mutates anything. Any other failure of the check is not a verdict and lets the caller proceed.
|
|
38
|
+
*/
|
|
39
|
+
export async function ensureAgentRuntimeCurrent(sidecar) {
|
|
40
|
+
if (!sidecar)
|
|
41
|
+
return;
|
|
42
|
+
try {
|
|
43
|
+
await sidecar.call('runtime.ensure_current', {});
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof SidecarError && error.condition === 'agent_runtime_stale')
|
|
47
|
+
throw staleRuntimeFailure(error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function staleRuntimeFailure(error) {
|
|
51
|
+
return new HttpFailure(409, error.message, { type: 'agent_runtime_stale', retryable: true, restart_scheduled: false, ...(error.data.agent_update_state !== undefined ? { agent_update_state: error.data.agent_update_state } : {}) });
|
|
52
|
+
}
|
|
28
53
|
const isDict = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
29
54
|
export class SessionService {
|
|
30
55
|
deps;
|
|
56
|
+
/** TAL-255: one manual compression worker per session, shared by every compress route. */
|
|
57
|
+
compressionJobs;
|
|
31
58
|
constructor(deps) {
|
|
32
59
|
this.deps = deps;
|
|
60
|
+
this.compressionJobs = new CompressionJobs();
|
|
33
61
|
}
|
|
34
62
|
get store() { return this.deps.store; }
|
|
63
|
+
/**
|
|
64
|
+
* TAL-186: rewrites a session's media references to their URLs. A local path is rewritten only when `/api/media`
|
|
65
|
+
* serves it for this session, under the same resolution, so every rewritten reference loads.
|
|
66
|
+
*/
|
|
67
|
+
mediaProjector(s) {
|
|
68
|
+
const media = this.deps.media;
|
|
69
|
+
const served = new Map();
|
|
70
|
+
const localUrl = (path) => {
|
|
71
|
+
if (!media?.localIo(s.profile))
|
|
72
|
+
return null;
|
|
73
|
+
if (served.has(path))
|
|
74
|
+
return served.get(path) ?? null;
|
|
75
|
+
let url = null;
|
|
76
|
+
try {
|
|
77
|
+
const target = mediaTarget(mediaRefPath(path));
|
|
78
|
+
if (mediaAnchorRoot(target, s, media.access))
|
|
79
|
+
url = `./api/media?${new URLSearchParams({ path: target, session_id: s.session_id }).toString()}`;
|
|
80
|
+
}
|
|
81
|
+
catch { /* an invalid path stays text */ }
|
|
82
|
+
served.set(path, url);
|
|
83
|
+
return url;
|
|
84
|
+
};
|
|
85
|
+
const workspace = s.workspace.trim() || null;
|
|
86
|
+
// A scene's final answer usually repeats its row's text: each distinct text is parsed once.
|
|
87
|
+
const projected = new Map();
|
|
88
|
+
return (text) => {
|
|
89
|
+
if (!projected.has(text))
|
|
90
|
+
projected.set(text, projectMediaRefs(text, { workspace, localUrl }));
|
|
91
|
+
return projected.get(text) ?? null;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
35
94
|
publish(reason, profile, sessionId) {
|
|
36
95
|
this.deps.events.publish(reason, { profile: profile ?? null, sessionId: sessionId ?? null });
|
|
37
96
|
}
|
|
@@ -135,16 +194,33 @@ export class SessionService {
|
|
|
135
194
|
* rows (a WebUI conversation continued from the CLI shows the CLI turns). This is the coordinate space `GET
|
|
136
195
|
* /api/session` exposes, so branching and the next model history slice/extend the same list.
|
|
137
196
|
*/
|
|
138
|
-
mergedTranscript(s, local = s.messages) {
|
|
139
|
-
const stateRows = this.stateDbRows(s);
|
|
197
|
+
mergedTranscript(s, local = s.messages, stateRows = this.stateDbRows(s)) {
|
|
140
198
|
if (!stateRows.length)
|
|
141
199
|
return local;
|
|
142
|
-
return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark });
|
|
200
|
+
return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed });
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Python `reconciled_state_db_messages_for_session(prefer_context=True)`: the model history is the owner context
|
|
204
|
+
* extended append-only with the Agent's state.db rows (a CLI continuation of this session reaches the model), except
|
|
205
|
+
* for a compressed context whose anchor cannot be verified — that stays context-only.
|
|
206
|
+
*/
|
|
207
|
+
modelContext(s, stateRows) {
|
|
208
|
+
const local = s.context_messages.length ? s.context_messages : s.messages.filter((m) => !m._error && !m._partial);
|
|
209
|
+
return local.some((m) => isContextCompressionMarker(m)) ? local : this.mergedTranscript(s, local, stateRows ?? this.stateDbRows(s));
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* TAL-518: the model context a `/btw` clone inherits: the chat's history plus its running turn's prompt, which
|
|
213
|
+
* deferred save keeps out of `messages` and `context_messages` until settlement.
|
|
214
|
+
*/
|
|
215
|
+
sideQuestionContext(s) {
|
|
216
|
+
const context = structuredClone(this.modelContext(s));
|
|
217
|
+
const pending = this.pendingTurn(s);
|
|
218
|
+
return pending ? withPendingUserTurn(context, pending) : context;
|
|
143
219
|
}
|
|
144
220
|
/** Python `_lookup_cli_session_metadata`: the sidebar row for a state.db session in the active profile. */
|
|
145
221
|
lookupCliMeta(sid) {
|
|
146
222
|
try {
|
|
147
|
-
return this.deps.cliSessions(this.deps.activeProfile(), { sourceFilter: null }).find((r) => str(r.session_id) === sid) ?? null;
|
|
223
|
+
return this.deps.cliSessions(this.deps.activeProfile(), { sourceFilter: null }).rows.find((r) => str(r.session_id) === sid) ?? null;
|
|
148
224
|
}
|
|
149
225
|
catch {
|
|
150
226
|
return null;
|
|
@@ -214,6 +290,10 @@ export class SessionService {
|
|
|
214
290
|
messages: msgs, created_at: Number(meta.created_at) || 0, updated_at: Number(meta.updated_at) || 0, profile: str(meta.profile) || null,
|
|
215
291
|
is_cli_session: claimable ? true : !subagentChild, source_tag: str(meta.source_tag) || null, raw_source: str(meta.raw_source) || null,
|
|
216
292
|
session_source: str(meta.session_source) || null, source_label: str(meta.source_label) || null, read_only: !claimable,
|
|
293
|
+
// Python `import_cli_session`: the claimed sidecar keeps the row's lineage, background project and channel identity
|
|
294
|
+
// (identity persists through `Session.extra`).
|
|
295
|
+
parent_session_id: str(meta.parent_session_id) || null, project_id: str(meta.project_id) || null,
|
|
296
|
+
...Object.fromEntries(CLI_IDENTITY_FIELDS.filter((k) => meta[k] != null && meta[k] !== '').map((k) => [k, meta[k]])),
|
|
217
297
|
}, defaults);
|
|
218
298
|
return { session, reason: claimable ? 'materialized' : 'not_claimable' };
|
|
219
299
|
}
|
|
@@ -233,17 +313,46 @@ export class SessionService {
|
|
|
233
313
|
if (this.isSubagentViewOnly(sid))
|
|
234
314
|
throw new HttpFailure(400, `Subagent sessions are view-only and cannot be ${verb} from WebUI`);
|
|
235
315
|
}
|
|
316
|
+
/** TAL-372: each delegation row of a turn's scene carries the status of the work it started. */
|
|
317
|
+
backgroundLinked(s, messages) {
|
|
318
|
+
return withBackgroundLinks(messages, this.deps.backgroundReceipts?.(s.session_id) ?? []);
|
|
319
|
+
}
|
|
236
320
|
/** `compact()` plus messages, redacted for the wire (Python `_public_session_projection`). */
|
|
237
321
|
publicSession(s, withMessages = true) {
|
|
238
322
|
const payload = this.wireRow(s);
|
|
239
323
|
// Mutation replies replace a client's transcript, so they carry the same server-built scenes as the detail.
|
|
240
324
|
if (withMessages)
|
|
241
|
-
payload.messages = hydrateAnchorActivityScenes(withToolCallOutcomes(withTurnIds(s.messages), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id });
|
|
325
|
+
payload.messages = this.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withMarkerKinds(withTurnIds(s.messages)), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, runningScene: !this.journaledActiveTurn(s) }));
|
|
242
326
|
return redactSessionData(payload, this.deps.redactEnabled());
|
|
243
327
|
}
|
|
244
328
|
/** `compact()` with the wire streaming/read-only flags (TAL-312), for replies that return the session row. */
|
|
245
329
|
wireRow(s) {
|
|
246
|
-
return withSessionWireFlags({ ...s.compact(), read_only: this.isReadOnly(s) }, this.deps.runtime.activeStreamIds);
|
|
330
|
+
return withSessionWireFlags({ ...s.compact({ contextLengthFor: this.deps.contextLengthFor, modelOptionFor: this.deps.modelOptionFor }), read_only: this.isReadOnly(s), assistant_name: this.assistantName(s), workspace_name: this.workspaceNames()(s) }, this.deps.runtime.activeStreamIds);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* TAL-303: the label for a session's workspace, from its own profile's registry (a profileless row is the root's).
|
|
334
|
+
* One resolver serves one response, so each profile's registry is read once however many rows it names.
|
|
335
|
+
*/
|
|
336
|
+
workspaceNames() {
|
|
337
|
+
const registries = new Map();
|
|
338
|
+
return (row) => {
|
|
339
|
+
const profile = str(row.profile) || 'default';
|
|
340
|
+
let entries = registries.get(profile);
|
|
341
|
+
if (!entries) {
|
|
342
|
+
try {
|
|
343
|
+
entries = this.deps.workspaces.entries(profile);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
entries = [];
|
|
347
|
+
}
|
|
348
|
+
registries.set(profile, entries);
|
|
349
|
+
}
|
|
350
|
+
return workspaceDisplayName(str(row.workspace), entries);
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** The agent's display name for a session's profile (`assistant_name`). */
|
|
354
|
+
assistantName(s) {
|
|
355
|
+
return this.deps.assistantName(s.profile);
|
|
247
356
|
}
|
|
248
357
|
/** Python `public_session_projection(s.__dict__)`: every persisted field, redacted (session export). */
|
|
249
358
|
publicSessionDocument(s) {
|
|
@@ -283,7 +392,7 @@ export class SessionService {
|
|
|
283
392
|
if (journaled)
|
|
284
393
|
transcript = withoutRunningTurnOutput(transcript, { ...journaled, localCount: s.messages.length });
|
|
285
394
|
// Turn ids, tool outcomes and scenes are computed over the full transcript, so every window reports the same values.
|
|
286
|
-
const all = loadMessages ? hydrateAnchorActivityScenes(withToolCallOutcomes(withTurnIds(withAttachmentObjects(transcript)), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, clipToolResults: msgLimit !== null }) : [];
|
|
395
|
+
const all = loadMessages ? this.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withMarkerKinds(withTurnIds(withAttachmentObjects(transcript))), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, runningScene: !journaled, clipToolResults: msgLimit !== null })) : [];
|
|
287
396
|
let truncated = [];
|
|
288
397
|
let offset = 0;
|
|
289
398
|
let summaryCount = null;
|
|
@@ -299,6 +408,8 @@ export class SessionService {
|
|
|
299
408
|
truncated = all.slice(prompt, offset + truncated.length);
|
|
300
409
|
offset = prompt;
|
|
301
410
|
}
|
|
411
|
+
// Per-row display fields (TAL-186 media, TAL-456 excerpts cut from that display text) only for the rows sent.
|
|
412
|
+
truncated = withBodyExcerpts(withDisplayMedia(truncated, this.mediaProjector(s)), s.active_stream_id);
|
|
302
413
|
if (msgLimit !== null)
|
|
303
414
|
truncated = messagesForLimitedPayload(truncated);
|
|
304
415
|
}
|
|
@@ -317,7 +428,7 @@ export class SessionService {
|
|
|
317
428
|
}
|
|
318
429
|
const activeStreamIds = this.deps.runtime.activeStreamIds;
|
|
319
430
|
const raw = {
|
|
320
|
-
...s.compact({ includeRuntime: true, activeStreamIds }),
|
|
431
|
+
...s.compact({ includeRuntime: true, activeStreamIds, contextLengthFor: this.deps.contextLengthFor, modelOptionFor: this.deps.modelOptionFor }),
|
|
321
432
|
messages: truncated,
|
|
322
433
|
message_count: mergedCount,
|
|
323
434
|
tool_calls: toolCalls,
|
|
@@ -344,7 +455,10 @@ export class SessionService {
|
|
|
344
455
|
const revisionAfter = this.loadRevision(s);
|
|
345
456
|
raw._load_revision = revisionBefore !== null && revisionBefore === revisionAfter ? hashRevision(revisionBefore) : `unstable-${randomUUID().replace(/-/g, '')}`;
|
|
346
457
|
raw.read_only = this.isReadOnly(s);
|
|
458
|
+
raw.assistant_name = this.assistantName(s);
|
|
459
|
+
raw.workspace_name = this.workspaceNames()(s);
|
|
347
460
|
withSessionWireFlags(raw, activeStreamIds);
|
|
461
|
+
raw.pending_steers = raw.active_stream_id ? (this.deps.runtime.pendingSteers?.(str(raw.active_stream_id)) ?? []) : [];
|
|
348
462
|
return redactSessionData(raw, this.deps.redactEnabled());
|
|
349
463
|
}
|
|
350
464
|
/**
|
|
@@ -390,7 +504,7 @@ export class SessionService {
|
|
|
390
504
|
foreignSessionDetail(sid) {
|
|
391
505
|
const { synth, meta } = this.foreignSession(sid);
|
|
392
506
|
// The same turn projection as a WebUI session: turn ids, tool outcomes, then each completed turn's scene.
|
|
393
|
-
const msgs = hydrateAnchorActivityScenes(withToolCallOutcomes(withTurnIds(synth.messages), [], null), {});
|
|
507
|
+
const msgs = hydrateAnchorActivityScenes(withToolCallOutcomes(withMarkerKinds(withTurnIds(synth.messages)), [], null), {});
|
|
394
508
|
const lastTs = Number(msgs[msgs.length - 1]?.timestamp ?? 0) || 0;
|
|
395
509
|
const sess = {
|
|
396
510
|
session_id: synth.session_id, title: synth.title, workspace: synth.workspace, model: synth.model, message_count: msgs.length,
|
|
@@ -398,6 +512,7 @@ export class SessionService {
|
|
|
398
512
|
pinned: synth.pinned, archived: synth.archived, project_id: synth.project_id ?? null, profile: synth.profile,
|
|
399
513
|
is_cli_session: synth.is_cli_session, source_tag: synth.source_tag, raw_source: synth.raw_source, session_source: synth.session_source,
|
|
400
514
|
source_label: synth.source_label, read_only: synth.read_only, can_duplicate: false, messages: msgs, tool_calls: [], transcript_seq: null,
|
|
515
|
+
assistant_name: this.assistantName(synth), workspace_name: this.workspaceNames()(synth),
|
|
401
516
|
};
|
|
402
517
|
attachTodoState(sess, msgs);
|
|
403
518
|
const merged = withSessionWireFlags(meta ? mergeCliSidebarMetadata(sess, meta) : sess, this.deps.runtime.activeStreamIds);
|
|
@@ -413,7 +528,7 @@ export class SessionService {
|
|
|
413
528
|
if (this.deps.runtime.activeRunStream(session.session_id))
|
|
414
529
|
return false;
|
|
415
530
|
const pendingAge = session.pending_started_at ? this.deps.now() - session.pending_started_at : null;
|
|
416
|
-
if (session.
|
|
531
|
+
if (session.hasPendingPrompt && pendingAge !== null && pendingAge < 30)
|
|
417
532
|
return false;
|
|
418
533
|
let target = session;
|
|
419
534
|
if (session.loadedMetadataOnly) {
|
|
@@ -433,7 +548,7 @@ export class SessionService {
|
|
|
433
548
|
// Python `_materialize_pending_user_turn_before_error` (#1361): the prompt that was in flight becomes a durable user
|
|
434
549
|
// turn and an interruption marker follows it, so a dead stream never silently drops what the user sent.
|
|
435
550
|
const pendingText = str(target.pending_user_message);
|
|
436
|
-
if (
|
|
551
|
+
if (target.hasPendingPrompt) {
|
|
437
552
|
const turnId = str(target.active_stream_id);
|
|
438
553
|
const startedAt = typeof target.pending_started_at === 'number' && target.pending_started_at > 0 ? target.pending_started_at : this.deps.now();
|
|
439
554
|
const attachments = [...target.pending_attachments];
|
|
@@ -466,10 +581,12 @@ export class SessionService {
|
|
|
466
581
|
const activeProfile = this.deps.activeProfile();
|
|
467
582
|
const wantState = params.showCliSessions || params.showCronSessions || params.showWebhookSessions || params.showKanbanSessions;
|
|
468
583
|
// Python reads every profile's state.db under all_profiles; this port projects the active profile only.
|
|
469
|
-
const
|
|
584
|
+
const cliRead = wantState ? this.deps.cliSessions(activeProfile, { sourceFilter: params.sourceFilter ?? null }) : undefined;
|
|
585
|
+
const cliRows = cliRead?.rows;
|
|
470
586
|
const gatewayIdentity = loadGatewaySessionIdentityMap(join(this.deps.profileHome(activeProfile), 'sessions', 'sessions.json'));
|
|
471
|
-
const
|
|
472
|
-
|
|
587
|
+
const truncatedSources = cliRead?.truncated;
|
|
588
|
+
const payload = buildSessionListPayload(this.store, { ...params, ...(cliRows ? { cliRows } : {}), ...(truncatedSources ? { truncatedSources } : {}), gatewayIdentity, stateDbSources: this.stateDbSources, activeProfile, isolatedProfileMode: this.deps.isolatedProfileMode(), profilesMatch: this.deps.profilesMatch });
|
|
589
|
+
return sessionListResponse(payload, this.deps.runtime, this.deps.redactEnabled(), this.deps.now(), this.workspaceNames());
|
|
473
590
|
}
|
|
474
591
|
search(q, opts) {
|
|
475
592
|
const activeProfile = this.deps.activeProfile();
|
|
@@ -478,8 +595,10 @@ export class SessionService {
|
|
|
478
595
|
sessions = sessions.filter((r) => this.deps.profilesMatch(str(r.profile) || null, activeProfile));
|
|
479
596
|
sessions = withOwnerLocks(sessions, this.stateDbSources);
|
|
480
597
|
const redact = this.deps.redactEnabled();
|
|
598
|
+
const workspaceName = this.workspaceNames();
|
|
481
599
|
const redactRow = (item) => {
|
|
482
600
|
withSessionWireFlags(item, this.deps.runtime.activeStreamIds);
|
|
601
|
+
item.workspace_name = workspaceName(item);
|
|
483
602
|
if (typeof item.title === 'string')
|
|
484
603
|
item.title = redactText(item.title, redact);
|
|
485
604
|
for (const f of ['display_title', '_state_db_title', 'parent_title'])
|
|
@@ -498,27 +617,61 @@ export class SessionService {
|
|
|
498
617
|
}
|
|
499
618
|
if (!opts.content)
|
|
500
619
|
continue;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
620
|
+
const hit = this.contentMatch(str(s.session_id), query, opts.depth);
|
|
621
|
+
if (hit)
|
|
622
|
+
results.push(redactRow({ ...s, ...hit }));
|
|
623
|
+
}
|
|
624
|
+
return { sessions: results, query, count: results.length, all_profiles: opts.allProfiles, active_profile: activeProfile };
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* TAL-308: the sidebar search. The candidates are exactly the `/api/sessions` rows for `params` (visibility, source,
|
|
628
|
+
* archived, projection and order), narrowed to `projectId` (`none`: rows without a project), then matched on the
|
|
629
|
+
* title, then the metadata the row shows (workspace path and name, model, provider, profile, source label), then message content.
|
|
630
|
+
*/
|
|
631
|
+
sidebarSearch(q, params, opts) {
|
|
632
|
+
let rows = this.list(params).body.sessions;
|
|
633
|
+
if (opts.projectId === 'none')
|
|
634
|
+
rows = rows.filter((r) => !r.project_id);
|
|
635
|
+
else if (opts.projectId)
|
|
636
|
+
rows = rows.filter((r) => str(r.project_id) === opts.projectId);
|
|
637
|
+
const query = q.toLowerCase().trim();
|
|
638
|
+
const base = { all_profiles: params.allProfiles, active_profile: this.deps.activeProfile(), include_archived: params.includeArchived, sidebar_filtered: true };
|
|
639
|
+
if (!query)
|
|
640
|
+
return { ...base, sessions: rows };
|
|
641
|
+
const results = [];
|
|
642
|
+
for (const row of rows) {
|
|
643
|
+
if (str(row.title).toLowerCase().includes(query))
|
|
644
|
+
results.push({ ...row, match_type: 'title' });
|
|
645
|
+
else if (['workspace', 'workspace_name', 'model', 'model_provider', 'profile', 'source_label'].some((k) => str(row[k]).toLowerCase().includes(query)))
|
|
646
|
+
results.push({ ...row, match_type: 'metadata' });
|
|
647
|
+
else if (opts.content) {
|
|
648
|
+
const hit = this.contentMatch(str(row.session_id), query, opts.depth);
|
|
649
|
+
if (hit)
|
|
650
|
+
results.push({ ...row, ...hit });
|
|
504
651
|
}
|
|
505
|
-
|
|
652
|
+
}
|
|
653
|
+
return { ...base, sessions: results, query, count: results.length };
|
|
654
|
+
}
|
|
655
|
+
/** The first of a stored session's first `depth` messages (0: all) containing `query`, with its redacted excerpt. */
|
|
656
|
+
contentMatch(sid, query, depth) {
|
|
657
|
+
let sess;
|
|
658
|
+
try {
|
|
659
|
+
sess = this.store.get(sid, { promote: false, cacheOnMiss: false });
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
for (const m of depth ? sess.messages.slice(0, depth) : sess.messages) {
|
|
665
|
+
const c = sessionSearchMessageText(m);
|
|
666
|
+
if (!c.toLowerCase().includes(query))
|
|
506
667
|
continue;
|
|
507
|
-
}
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
const item = { ...s, match_type: 'content' };
|
|
513
|
-
const preview = sessionSearchPreview(c, query);
|
|
514
|
-
if (preview)
|
|
515
|
-
item.match_preview = redactText(preview, redact);
|
|
516
|
-
results.push(redactRow(item));
|
|
517
|
-
break;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
668
|
+
const item = { match_type: 'content' };
|
|
669
|
+
const preview = sessionSearchPreview(c, query);
|
|
670
|
+
if (preview)
|
|
671
|
+
item.match_preview = redactText(preview, this.deps.redactEnabled());
|
|
672
|
+
return item;
|
|
520
673
|
}
|
|
521
|
-
return
|
|
674
|
+
return null;
|
|
522
675
|
}
|
|
523
676
|
// ── status / usage ───────────────────────────────────────────────────────
|
|
524
677
|
status(sid) {
|
|
@@ -535,10 +688,10 @@ export class SessionService {
|
|
|
535
688
|
catch {
|
|
536
689
|
hermesHome = '';
|
|
537
690
|
}
|
|
538
|
-
const { is_streaming: streaming, active_stream_id: live, read_only: readOnly } = this.wireRow(full);
|
|
691
|
+
const { is_streaming: streaming, active_stream_id: live, read_only: readOnly, active_turn_origin: origin } = this.wireRow(full);
|
|
539
692
|
return {
|
|
540
693
|
session_id: full.session_id, title: full.title, model: full.model, profile, hermes_home: hermesHome, workspace: full.workspace, personality: full.personality,
|
|
541
|
-
message_count: full.messages.length, created_at: full.created_at, updated_at: full.updated_at, agent_running: streaming, is_streaming: streaming, active_stream_id: live, read_only: readOnly,
|
|
694
|
+
message_count: full.messages.length, created_at: full.created_at, updated_at: full.updated_at, agent_running: streaming, is_streaming: streaming, active_stream_id: live, active_turn_origin: origin, read_only: readOnly,
|
|
542
695
|
input_tokens: inp, output_tokens: out, total_tokens: inp + out, estimated_cost: full.estimated_cost,
|
|
543
696
|
};
|
|
544
697
|
}
|
|
@@ -578,7 +731,8 @@ export class SessionService {
|
|
|
578
731
|
return toolsets;
|
|
579
732
|
}
|
|
580
733
|
create(body, opts = {}) {
|
|
581
|
-
|
|
734
|
+
// Python `new_session`: a body without a profile takes the request's active profile (cookie first).
|
|
735
|
+
const profile = (typeof body.profile === 'string' && body.profile) || this.deps.activeProfile();
|
|
582
736
|
let prevSessionId = typeof body.prev_session_id === 'string' && body.prev_session_id ? body.prev_session_id : null;
|
|
583
737
|
if (prevSessionId && !this.sessionIdVisible(prevSessionId))
|
|
584
738
|
prevSessionId = null;
|
|
@@ -774,6 +928,176 @@ export class SessionService {
|
|
|
774
928
|
this.deps.runtime.evictAgent(sid);
|
|
775
929
|
return { ok: true, session: this.publicSession(s) };
|
|
776
930
|
}
|
|
931
|
+
// ── manual compression (TAL-255) ────────────────────────────────────────
|
|
932
|
+
/**
|
|
933
|
+
* Python `_handle_session_compress_start`: validate, then join the session's running job or start a fresh one (a
|
|
934
|
+
* finished job is replaced). The stale-runtime refusal comes before a job exists; a running job is joined without it.
|
|
935
|
+
*/
|
|
936
|
+
async startCompression(sid, focusRaw) {
|
|
937
|
+
const running = () => { const job = this.compressionJobs.get(sid); return job?.status === 'running' ? job : undefined; };
|
|
938
|
+
// A running job is joined first: once it has installed its short context, the guards below would refuse a new one.
|
|
939
|
+
const existing = running();
|
|
940
|
+
if (existing)
|
|
941
|
+
return existing;
|
|
942
|
+
const { s } = this.compressionTarget(sid);
|
|
943
|
+
const focusTopic = str(focusRaw).trim().slice(0, 500) || null;
|
|
944
|
+
await ensureAgentRuntimeCurrent(this.deps.sidecar?.() ?? null);
|
|
945
|
+
// Another start may have admitted a job while the runtime check awaited.
|
|
946
|
+
const admitted = running();
|
|
947
|
+
if (admitted)
|
|
948
|
+
return admitted;
|
|
949
|
+
// The detached job (and its finalize) is the profile's activity until it settles; checked and taken in one step.
|
|
950
|
+
const profile = s.profile ?? null;
|
|
951
|
+
if (this.deps.profileDeleting?.(profile))
|
|
952
|
+
throw new HttpFailure(409, `Profile '${str(profile)}' is being deleted.`);
|
|
953
|
+
const release = this.deps.profileActivity?.(profile);
|
|
954
|
+
const now = this.deps.now();
|
|
955
|
+
const job = { session_id: sid, focus_topic: focusTopic, status: 'running', started_at: now, updated_at: now, done: Promise.resolve() };
|
|
956
|
+
job.done = this.compressSession(sid, focusTopic).finally(() => { release?.(); this.compressionJobs.expireLater(job); }).then((result) => { Object.assign(job, { status: 'done', result, updated_at: this.deps.now() }); }, (error) => {
|
|
957
|
+
const known = error instanceof HttpFailure;
|
|
958
|
+
if (!known)
|
|
959
|
+
this.deps.log(`[webui] Manual compression worker failed for session ${sid}: ${error.message}`);
|
|
960
|
+
Object.assign(job, { status: 'error', error: known ? error.message : `Compression failed: ${sanitizePaths(error)}`, error_status: known ? error.status : 500, error_extra: known ? error.extra : {}, updated_at: this.deps.now() });
|
|
961
|
+
});
|
|
962
|
+
this.compressionJobs.set(job);
|
|
963
|
+
return job;
|
|
964
|
+
}
|
|
965
|
+
/** The session a compression may run on now, and the model history it compresses: the context a turn would send, so a
|
|
966
|
+
* repeat compression builds on the previous summary instead of re-reading the whole display transcript. */
|
|
967
|
+
compressionTarget(sid) {
|
|
968
|
+
this.rejectSubagent(sid, 'compressed');
|
|
969
|
+
const s = this.mutationTarget(sid, 'compressed');
|
|
970
|
+
if (s.active_stream_id)
|
|
971
|
+
throw new HttpFailure(409, 'Session is still streaming; wait for the current turn to finish.');
|
|
972
|
+
const history = sanitizeMessagesForApi(this.modelContext(s));
|
|
973
|
+
if (history.length < 4)
|
|
974
|
+
throw new HttpFailure(400, 'Not enough conversation to compress (need at least 4 messages).');
|
|
975
|
+
return { s, history };
|
|
976
|
+
}
|
|
977
|
+
/** The session's compression job; a deleted session's finished result is dropped rather than served. */
|
|
978
|
+
compressionJob(sid) {
|
|
979
|
+
try {
|
|
980
|
+
this.store.get(sid, { metadataOnly: true });
|
|
981
|
+
}
|
|
982
|
+
catch {
|
|
983
|
+
this.compressionJobs.delete(sid);
|
|
984
|
+
throw new HttpFailure(404, 'Session not found');
|
|
985
|
+
}
|
|
986
|
+
return this.compressionJobs.get(sid);
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Python `_handle_session_compress`: compress the sanitized transcript in the sidecar outside the lock, then under the
|
|
990
|
+
* session lock refuse a result the session moved past (stream state or transcript changed) and install it as the
|
|
991
|
+
* model context with the manual anchor and the #4836 boundary that keeps state.db from replaying compressed rows.
|
|
992
|
+
* The guards run again first: a stream may have started while the caller awaited the runtime check.
|
|
993
|
+
*/
|
|
994
|
+
async compressSession(sid, focusTopic) {
|
|
995
|
+
const { s, history } = this.compressionTarget(sid);
|
|
996
|
+
// The display transcript and the model context must both be where the compression found them; the key starts from
|
|
997
|
+
// the exact history sent, so a state.db row that lands after this read fails the commit instead of being dropped.
|
|
998
|
+
const transcriptKey = (x, context) => JSON.stringify([sanitizeMessagesForApi(x.messages), context]);
|
|
999
|
+
const historyKey = transcriptKey(s, history);
|
|
1000
|
+
const streamState = (x) => JSON.stringify([x.active_stream_id ?? null, x.pending_user_message ?? null, x.pending_attachments ?? null, x.pending_started_at ?? null]);
|
|
1001
|
+
const streamBefore = streamState(s);
|
|
1002
|
+
const sidecar = this.deps.sidecar?.();
|
|
1003
|
+
if (!sidecar)
|
|
1004
|
+
throw new HttpFailure(400, 'Compression failed: the Agent sidecar is not running');
|
|
1005
|
+
const [model, provider] = this.deps.modelStateFromRequest(s.model, s.model_provider, s.model_provider);
|
|
1006
|
+
let result;
|
|
1007
|
+
try {
|
|
1008
|
+
result = await sidecar.call('chat.compress', {
|
|
1009
|
+
profile_home: this.deps.profileHome(s.profile ?? this.deps.activeProfile()), session_id: sid, model: model ?? '', model_provider: provider,
|
|
1010
|
+
conversation_history: history, focus_topic: focusTopic, enabled_toolsets: s.enabled_toolsets,
|
|
1011
|
+
}, { timeoutMs: 0 });
|
|
1012
|
+
}
|
|
1013
|
+
catch (error) {
|
|
1014
|
+
if (error instanceof SidecarError && error.condition === 'agent_runtime_stale')
|
|
1015
|
+
throw staleRuntimeFailure(error);
|
|
1016
|
+
if (error instanceof SidecarError && error.condition === 'credential_missing')
|
|
1017
|
+
throw new HttpFailure(400, 'No provider configured -- cannot compress.');
|
|
1018
|
+
throw new HttpFailure(400, `Compression failed: ${sanitizePaths(error)}`);
|
|
1019
|
+
}
|
|
1020
|
+
// A held compression lock is a conflict to retry; `nothing_to_do` is the Agent's verdict on this transcript.
|
|
1021
|
+
if (result.status !== 'compressed')
|
|
1022
|
+
throw new HttpFailure(result.status === 'lock_skipped' ? 409 : 400, result.message ?? 'Nothing to compress yet.');
|
|
1023
|
+
const summary = result.summary ?? {};
|
|
1024
|
+
let committed = false;
|
|
1025
|
+
try {
|
|
1026
|
+
const current = await this.store.withLock(sid, () => {
|
|
1027
|
+
let live;
|
|
1028
|
+
try {
|
|
1029
|
+
live = this.store.get(sid);
|
|
1030
|
+
}
|
|
1031
|
+
catch {
|
|
1032
|
+
throw new HttpFailure(404, 'Session not found');
|
|
1033
|
+
}
|
|
1034
|
+
if (streamState(live) !== streamBefore)
|
|
1035
|
+
throw new HttpFailure(409, 'Session stream state changed during compression; please retry.');
|
|
1036
|
+
// One state.db read serves the check, the kept display rows, and the boundary.
|
|
1037
|
+
const stateRows = this.stateDbRows(live);
|
|
1038
|
+
if (transcriptKey(live, sanitizeMessagesForApi(this.modelContext(live, stateRows))) !== historyKey)
|
|
1039
|
+
throw new HttpFailure(409, 'Session was modified during compression; please retry.');
|
|
1040
|
+
// Rows the sidecar returns without a timestamp take the newest one this compression saw, never the clock: the
|
|
1041
|
+
// boundary then hides no state.db row the append-only merge would still show (it already drops rows at or
|
|
1042
|
+
// before the newest local row), so a CLI row committed after the read follows the compressed context.
|
|
1043
|
+
const seen = [...live.messages, ...live.context_messages, ...stateRows].map((m) => Number(m.timestamp)).filter(Number.isFinite);
|
|
1044
|
+
const stamp = seen.length ? Math.max(...seen) : this.deps.now();
|
|
1045
|
+
const compressed = copyJson(result.messages);
|
|
1046
|
+
for (const m of compressed)
|
|
1047
|
+
m.timestamp ??= stamp;
|
|
1048
|
+
live.context_messages = compressed;
|
|
1049
|
+
live.active_stream_id = null;
|
|
1050
|
+
live.pending_user_message = null;
|
|
1051
|
+
live.pending_attachments = [];
|
|
1052
|
+
live.pending_started_at = null;
|
|
1053
|
+
live.pending_user_source = null;
|
|
1054
|
+
// The compressed history included any state.db-only continuation; the new boundary would hide those rows from
|
|
1055
|
+
// the display, so the transcript keeps them before it is applied (and the anchor counts them).
|
|
1056
|
+
const display = this.mergedTranscript(live, live.messages, stateRows);
|
|
1057
|
+
if (display.length > live.messages.length)
|
|
1058
|
+
live.messages = copyJson(display);
|
|
1059
|
+
const visible = visibleMessagesForAnchor(live.messages);
|
|
1060
|
+
live.compression_anchor_visible_idx = visible.length ? visible.length - 1 : null;
|
|
1061
|
+
live.compression_anchor_message_key = anchorMessageKey(visible.at(-1));
|
|
1062
|
+
live.compression_anchor_summary = anchorSummary(summary, compressed);
|
|
1063
|
+
live.compression_anchor_mode = 'manual';
|
|
1064
|
+
// #4836: an intentional-shrink boundary, so append-only state.db reconciliation does not replay compressed rows.
|
|
1065
|
+
live.truncation_watermark = truncationWatermarkFor(compressed);
|
|
1066
|
+
live.truncation_boundary = live.truncation_watermark;
|
|
1067
|
+
live.truncation_watermark_compressed = true;
|
|
1068
|
+
live.last_prompt_tokens = result.after_tokens;
|
|
1069
|
+
live.post_compression_context_tokens_estimate = result.after_tokens;
|
|
1070
|
+
this.store.save(live);
|
|
1071
|
+
// A backup from before the compression would restore the uncompressed context.
|
|
1072
|
+
try {
|
|
1073
|
+
rmSync(`${this.store.pathFor(sid)}.bak`, { force: true });
|
|
1074
|
+
}
|
|
1075
|
+
catch { /* ignore */ }
|
|
1076
|
+
return live;
|
|
1077
|
+
});
|
|
1078
|
+
committed = true;
|
|
1079
|
+
// The cached turn agent still carries the uncompressed state; the next turn builds a fresh one (gateway parity).
|
|
1080
|
+
// Awaited, so a turn sent right after the reply cannot reuse the old agent.
|
|
1081
|
+
try {
|
|
1082
|
+
await sidecar.call('chat.evict_agent', { session_id: sid });
|
|
1083
|
+
}
|
|
1084
|
+
catch (error) {
|
|
1085
|
+
this.deps.log(`[webui] agent eviction after compression of ${sid} failed: ${error.message}`);
|
|
1086
|
+
}
|
|
1087
|
+
return { ok: true, session: this.publicSession(current), summary, focus_topic: focusTopic };
|
|
1088
|
+
}
|
|
1089
|
+
finally {
|
|
1090
|
+
// Second phase: the Agent's context-engine notification fires only for a result the session now holds.
|
|
1091
|
+
if (result.commit_token) {
|
|
1092
|
+
try {
|
|
1093
|
+
await sidecar.call('chat.compress_finalize', { commit_token: result.commit_token, committed });
|
|
1094
|
+
}
|
|
1095
|
+
catch (error) {
|
|
1096
|
+
this.deps.log(`[webui] compression finalize for ${sid} failed: ${error.message}`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
777
1101
|
async clear(sid) {
|
|
778
1102
|
this.rejectSubagent(sid, 'modified');
|
|
779
1103
|
const s = this.mutationTarget(sid, 'modified');
|
|
@@ -810,7 +1134,7 @@ export class SessionService {
|
|
|
810
1134
|
catch { /* ignore */ }
|
|
811
1135
|
}
|
|
812
1136
|
});
|
|
813
|
-
this.deps.runtime.evictAgent(sid);
|
|
1137
|
+
this.deps.runtime.evictAgent(sid, true);
|
|
814
1138
|
return { ok: true, session: this.wireRow(s) };
|
|
815
1139
|
}
|
|
816
1140
|
async retry(sid) {
|
|
@@ -821,11 +1145,19 @@ export class SessionService {
|
|
|
821
1145
|
const lastUser = findLastUserIndex(history);
|
|
822
1146
|
if (lastUser === null)
|
|
823
1147
|
return { error: 'No previous message to retry.' };
|
|
824
|
-
|
|
1148
|
+
// What a client resends (TAL-515): the files that reached the model, which only ever got attachments with a path,
|
|
1149
|
+
// and the prompt as typed. The attached-files line is the server's only when such files exist; otherwise the user
|
|
1150
|
+
// typed it. Nothing to resend fails before the exchange is removed.
|
|
1151
|
+
const prompt = history[lastUser];
|
|
1152
|
+
const lastUserText = extractText(prompt.content);
|
|
1153
|
+
const lastUserAttachments = attachmentObjects(Array.isArray(prompt.attachments) ? prompt.attachments : []).filter((a) => isDict(a) && Boolean(str(a.path).trim()));
|
|
1154
|
+
const lastUserPrompt = stripWorkspacePrefix(lastUserAttachments.length ? stripAttachedFilesMarker(lastUserText) : lastUserText, true);
|
|
1155
|
+
if (!lastUserPrompt && !lastUserAttachments.length)
|
|
1156
|
+
return { error: 'The last message has nothing to resend.' };
|
|
825
1157
|
const removed = history.length - lastUser;
|
|
826
1158
|
shrinkTo(s, lastUser);
|
|
827
1159
|
this.store.save(s);
|
|
828
|
-
return { ok: true, last_user_text: lastUserText, removed_count: removed };
|
|
1160
|
+
return { ok: true, last_user_text: lastUserText, last_user_prompt: lastUserPrompt, last_user_attachments: lastUserAttachments, removed_count: removed };
|
|
829
1161
|
});
|
|
830
1162
|
}
|
|
831
1163
|
async undo(sid) {
|
|
@@ -850,12 +1182,14 @@ export class SessionService {
|
|
|
850
1182
|
if (!session)
|
|
851
1183
|
throw new HttpFailure(404, 'Session not found');
|
|
852
1184
|
const now = this.deps.now();
|
|
1185
|
+
// A truncation cut names deleted state.db rows of the source; the copy's own state.db rows are all genuine (TAL-504).
|
|
1186
|
+
const watermark = session.truncation_watermark_compressed ? { truncation_watermark: session.truncation_watermark, truncation_boundary: session.truncation_boundary, truncation_watermark_compressed: true } : {};
|
|
853
1187
|
const copied = new Session({
|
|
854
1188
|
title: `${session.title || 'Untitled'} (copy)`, workspace: session.workspace, model: session.model, model_provider: session.model_provider,
|
|
855
1189
|
messages: copyJson(session.messages), tool_calls: copyJson(session.tool_calls), pinned: false, archived: false, project_id: session.project_id, profile: session.profile,
|
|
856
1190
|
input_tokens: session.input_tokens, output_tokens: session.output_tokens, estimated_cost: session.estimated_cost, cache_read_tokens: session.cache_read_tokens, cache_write_tokens: session.cache_write_tokens,
|
|
857
1191
|
personality: session.personality, enabled_toolsets: session.enabled_toolsets, context_length: session.context_length, threshold_tokens: session.threshold_tokens,
|
|
858
|
-
|
|
1192
|
+
...watermark, context_messages: copyJson(session.context_messages),
|
|
859
1193
|
gateway_routing: copyJson(session.gateway_routing), gateway_routing_history: copyJson(session.gateway_routing_history), llm_title_generated: session.llm_title_generated,
|
|
860
1194
|
manual_title: session.manual_title, composer_draft: copyJson(session.composer_draft), context_engine: session.context_engine, context_engine_state: copyJson(session.context_engine_state),
|
|
861
1195
|
created_at: now, updated_at: now,
|
|
@@ -930,11 +1264,13 @@ export class SessionService {
|
|
|
930
1264
|
return {};
|
|
931
1265
|
} })();
|
|
932
1266
|
let eventProfile = null;
|
|
1267
|
+
let hadSidecar = true;
|
|
933
1268
|
try {
|
|
934
1269
|
eventProfile = this.store.get(sid, { metadataOnly: true }).profile;
|
|
935
1270
|
}
|
|
936
1271
|
catch {
|
|
937
1272
|
eventProfile = null;
|
|
1273
|
+
hadSidecar = false;
|
|
938
1274
|
}
|
|
939
1275
|
// Python `_is_messaging_session_id`: decided before the JSON is gone, from WebUI metadata or the Agent's row.
|
|
940
1276
|
const isMessaging = (() => { try {
|
|
@@ -961,6 +1297,14 @@ export class SessionService {
|
|
|
961
1297
|
await this.store.withLock(sid, () => {
|
|
962
1298
|
if (blocking())
|
|
963
1299
|
throw new HttpFailure(409, 'Session has an active run; stop it before deleting');
|
|
1300
|
+
// A public share outlives its session file, and revokeShare needs the session, so revoke it first.
|
|
1301
|
+
try {
|
|
1302
|
+
this.deps.shares.revoke(this.store.get(sid, { metadataOnly: true }));
|
|
1303
|
+
}
|
|
1304
|
+
catch (error) {
|
|
1305
|
+
if (!(error instanceof SessionNotFound))
|
|
1306
|
+
throw error;
|
|
1307
|
+
}
|
|
964
1308
|
if (!this.store.deleteFiles(sid))
|
|
965
1309
|
throw new HttpFailure(500, 'Failed to delete session data');
|
|
966
1310
|
}, { timeoutMs: 5000 });
|
|
@@ -970,7 +1314,7 @@ export class SessionService {
|
|
|
970
1314
|
throw new HttpFailure(503, 'Session busy, try again');
|
|
971
1315
|
throw error;
|
|
972
1316
|
}
|
|
973
|
-
this.deps.runtime.evictAgent(sid);
|
|
1317
|
+
this.deps.runtime.evictAgent(sid, true);
|
|
974
1318
|
try {
|
|
975
1319
|
rmSync(this.deps.attachmentDir(sid), { recursive: true, force: true });
|
|
976
1320
|
}
|
|
@@ -993,6 +1337,9 @@ export class SessionService {
|
|
|
993
1337
|
}
|
|
994
1338
|
}
|
|
995
1339
|
this.publish('session_delete', eventProfile);
|
|
1340
|
+
// Only a sidecar session is ever published to the relay.
|
|
1341
|
+
if (hadSidecar)
|
|
1342
|
+
this.deps.clearRelayCompletions?.(sid, eventProfile);
|
|
996
1343
|
return { ok: true, state_db_cleanup_failed: stateDbCleanupFailed, ...retained };
|
|
997
1344
|
}
|
|
998
1345
|
cleanup(zeroOnly) {
|
|
@@ -1002,6 +1349,13 @@ export class SessionService {
|
|
|
1002
1349
|
const shouldDelete = zeroOnly ? s.messages.length === 0 : s.title === 'Untitled' && s.messages.length === 0;
|
|
1003
1350
|
if (!shouldDelete)
|
|
1004
1351
|
continue;
|
|
1352
|
+
// A cleared session keeps its share; keep the session (and its revoke route) if the share cannot be revoked.
|
|
1353
|
+
try {
|
|
1354
|
+
this.deps.shares.revoke(s);
|
|
1355
|
+
}
|
|
1356
|
+
catch {
|
|
1357
|
+
continue;
|
|
1358
|
+
}
|
|
1005
1359
|
this.store.sessions.delete(s.session_id);
|
|
1006
1360
|
rmSync(this.store.pathFor(s.session_id), { force: true });
|
|
1007
1361
|
cleaned += 1;
|
|
@@ -1202,36 +1556,50 @@ export class SessionService {
|
|
|
1202
1556
|
return { ok: true, ...placement };
|
|
1203
1557
|
});
|
|
1204
1558
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
const messageRef = normalizeAnchorSceneMessageRef(query.message_ref);
|
|
1208
|
-
const messageIndex = anchorSceneIntOrNull(query.message_index);
|
|
1209
|
-
if (!sid || (!messageRef && messageIndex === null))
|
|
1210
|
-
throw new HttpFailure(400, 'session_id and message_ref or message_index are required');
|
|
1211
|
-
let session;
|
|
1212
|
-
let transcript;
|
|
1559
|
+
/** The session and the full transcript its detail is built from; a state.db-only session gives its synthesized one. */
|
|
1560
|
+
fullTranscript(sid) {
|
|
1213
1561
|
try {
|
|
1214
|
-
session = this.store.get(sid);
|
|
1562
|
+
let session = this.store.get(sid);
|
|
1215
1563
|
if (session.loadedMetadataOnly)
|
|
1216
1564
|
session = this.store.load(sid) ?? session;
|
|
1217
1565
|
if (!this.visibleToActiveProfile(session.profile))
|
|
1218
1566
|
throw new HttpFailure(404, 'Session not found');
|
|
1219
|
-
transcript
|
|
1567
|
+
return { session, transcript: this.mergedTranscript(session), stored: true };
|
|
1220
1568
|
}
|
|
1221
1569
|
catch (error) {
|
|
1222
1570
|
if (error instanceof HttpFailure)
|
|
1223
1571
|
throw error;
|
|
1224
|
-
|
|
1225
|
-
session
|
|
1226
|
-
transcript = session.messages;
|
|
1572
|
+
const session = this.foreignSession(sid).synth;
|
|
1573
|
+
return { session, transcript: session.messages, stored: false };
|
|
1227
1574
|
}
|
|
1575
|
+
}
|
|
1576
|
+
readAnchorScene(query) {
|
|
1577
|
+
const sid = str(query.session_id).trim();
|
|
1578
|
+
const messageRef = normalizeAnchorSceneMessageRef(query.message_ref);
|
|
1579
|
+
const messageIndex = anchorSceneIntOrNull(query.message_index);
|
|
1580
|
+
if (!sid || (!messageRef && messageIndex === null))
|
|
1581
|
+
throw new HttpFailure(400, 'session_id and message_ref or message_index are required');
|
|
1582
|
+
const { session, transcript, stored } = this.fullTranscript(sid);
|
|
1228
1583
|
const result = readAnchorSceneRows(session, { messageRef, messageIndex, before: anchorSceneIntOrNull(query.before), limit: anchorSceneIntOrNull(query.limit) }, withToolCallOutcomes(withTurnIds(transcript), session.tool_calls, session.active_stream_id));
|
|
1229
1584
|
if (!result)
|
|
1230
1585
|
throw new HttpFailure(404, 'Anchor activity scene not found');
|
|
1231
1586
|
// Paged rows come from the raw transcript, so they take the same credential redaction as the detail's preview.
|
|
1232
1587
|
const enabled = this.deps.redactEnabled();
|
|
1233
1588
|
const redacted = redactValue(result, enabled);
|
|
1234
|
-
|
|
1589
|
+
const rows = withSceneToolDisplay(result.rows, redacted.rows, enabled);
|
|
1590
|
+
// `/api/media` serves stored sessions only, so a state.db-only session's rows stay as written, as its detail does.
|
|
1591
|
+
return { ...redacted, rows: stored ? withSceneRowMedia(rows, this.mediaProjector(session)) : rows };
|
|
1592
|
+
}
|
|
1593
|
+
/** TAL-331: one tool call's whole result, which a limited response clipped (`result_truncated`), redacted like the detail. */
|
|
1594
|
+
readToolResult(query) {
|
|
1595
|
+
const sid = query.session_id.trim();
|
|
1596
|
+
const id = query.tool_call_id.trim();
|
|
1597
|
+
if (!sid || !id)
|
|
1598
|
+
throw new HttpFailure(400, 'session_id and tool_call_id are required');
|
|
1599
|
+
const full = fullToolResult(this.fullTranscript(sid).transcript, id);
|
|
1600
|
+
if (full === null)
|
|
1601
|
+
throw new HttpFailure(404, 'Tool result not found');
|
|
1602
|
+
return { tool_call_id: id, ...redactValue(full, this.deps.redactEnabled()) };
|
|
1235
1603
|
}
|
|
1236
1604
|
// ── shares ───────────────────────────────────────────────────────────────
|
|
1237
1605
|
createShare(sid) {
|
|
@@ -1316,6 +1684,10 @@ function findLastUserIndex(history) {
|
|
|
1316
1684
|
return i;
|
|
1317
1685
|
return null;
|
|
1318
1686
|
}
|
|
1687
|
+
/** Python `_sanitize_error`: absolute paths in an error message never reach the client. */
|
|
1688
|
+
export function sanitizePaths(error) {
|
|
1689
|
+
return str(error?.message ?? error).replace(/(?:(?:\/[a-zA-Z0-9_.-]+)+|(?:[A-Z]:\\[^\s]+))/g, '<path>');
|
|
1690
|
+
}
|
|
1319
1691
|
function truncationWatermarkFor(messages) {
|
|
1320
1692
|
if (!messages.length)
|
|
1321
1693
|
return 0.0;
|
|
@@ -1329,17 +1701,142 @@ function stampIntentionalShrink(session, oldCount, newCount) {
|
|
|
1329
1701
|
session.intentional_shrink_generation = randomUUID().replace(/-/g, '');
|
|
1330
1702
|
return true;
|
|
1331
1703
|
}
|
|
1332
|
-
/**
|
|
1704
|
+
/** Python truthiness for the JSON values a transcript row carries. */
|
|
1705
|
+
function pyTruthy(v) {
|
|
1706
|
+
if (Array.isArray(v))
|
|
1707
|
+
return v.length > 0;
|
|
1708
|
+
if (isDict(v))
|
|
1709
|
+
return Object.keys(v).length > 0;
|
|
1710
|
+
return Boolean(v);
|
|
1711
|
+
}
|
|
1712
|
+
/** JSON with sorted object keys (Python `json.dumps(..., sort_keys=True)`). */
|
|
1713
|
+
function sortedJson(v) {
|
|
1714
|
+
return JSON.stringify(v, (_key, value) => (isDict(value) ? Object.fromEntries(Object.keys(value).sort().map((k) => [k, value[k]])) : value)) ?? '';
|
|
1715
|
+
}
|
|
1716
|
+
/** Equality key for a row `id`/`timestamp`; `null` when the value is absent. Numbers compare by value (`2 == 2.0`). */
|
|
1717
|
+
function identityKey(v) {
|
|
1718
|
+
if (v === null || v === undefined)
|
|
1719
|
+
return null;
|
|
1720
|
+
return typeof v === 'string' ? `s${v}` : typeof v === 'number' ? `n${String(v)}` : `j${sortedJson(v)}`;
|
|
1721
|
+
}
|
|
1722
|
+
function rowSignature(row) {
|
|
1723
|
+
if (!isDict(row))
|
|
1724
|
+
return null;
|
|
1725
|
+
const field = (v) => (!pyTruthy(v) ? '' : typeof v === 'string' ? v : sortedJson(v));
|
|
1726
|
+
return JSON.stringify([field(row.role), field(row.content), field(row.tool_call_id), field(row.tool_use_id), field(row.tool_name || row.name), field(row.tool_calls)]);
|
|
1727
|
+
}
|
|
1728
|
+
/** First index in sorted `positions` whose value is `>= start`. */
|
|
1729
|
+
function lowerBound(positions, start) {
|
|
1730
|
+
let lo = 0;
|
|
1731
|
+
let hi = positions.length;
|
|
1732
|
+
while (lo < hi) {
|
|
1733
|
+
const mid = (lo + hi) >> 1;
|
|
1734
|
+
if (positions[mid] < start)
|
|
1735
|
+
lo = mid + 1;
|
|
1736
|
+
else
|
|
1737
|
+
hi = mid;
|
|
1738
|
+
}
|
|
1739
|
+
return lo;
|
|
1740
|
+
}
|
|
1741
|
+
/**
|
|
1742
|
+
* Map each display row to the context row it represents, scanning forward. An `id` match wins; otherwise the role+content
|
|
1743
|
+
* signature must match, and a shared `timestamp` makes it exact. A signature match where an identity is missing is weak:
|
|
1744
|
+
* one weak candidate counts as a match, a second one before any exact match leaves the row ambiguous.
|
|
1745
|
+
*/
|
|
1746
|
+
function alignDisplayToContext(ctx, full) {
|
|
1747
|
+
const index = () => new Map();
|
|
1748
|
+
const push = (map, key, idx) => {
|
|
1749
|
+
const list = map.get(key);
|
|
1750
|
+
if (list)
|
|
1751
|
+
list.push(idx);
|
|
1752
|
+
else
|
|
1753
|
+
map.set(key, [idx]);
|
|
1754
|
+
};
|
|
1755
|
+
const first = (list, start) => list?.[lowerBound(list, start)];
|
|
1756
|
+
const byId = index(), bySigTs = index(), bySigTsNoId = index();
|
|
1757
|
+
const weakAny = index(), weakNoId = index(), weakNoTs = index(), weakNoIdNoTs = index();
|
|
1758
|
+
ctx.forEach((row, idx) => {
|
|
1759
|
+
const sig = rowSignature(row);
|
|
1760
|
+
if (sig === null)
|
|
1761
|
+
return;
|
|
1762
|
+
const id = identityKey(row.id);
|
|
1763
|
+
const ts = identityKey(row.timestamp);
|
|
1764
|
+
push(weakAny, sig, idx);
|
|
1765
|
+
if (id === null)
|
|
1766
|
+
push(weakNoId, sig, idx);
|
|
1767
|
+
if (ts === null)
|
|
1768
|
+
push(weakNoTs, sig, idx);
|
|
1769
|
+
if (id === null && ts === null)
|
|
1770
|
+
push(weakNoIdNoTs, sig, idx);
|
|
1771
|
+
if (id !== null)
|
|
1772
|
+
push(byId, id, idx);
|
|
1773
|
+
if (ts !== null) {
|
|
1774
|
+
push(bySigTs, `${sig}\0${ts}`, idx);
|
|
1775
|
+
if (id === null)
|
|
1776
|
+
push(bySigTsNoId, `${sig}\0${ts}`, idx);
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
const matches = [];
|
|
1780
|
+
const ambiguous = [];
|
|
1781
|
+
let next = 0;
|
|
1782
|
+
for (const message of full) {
|
|
1783
|
+
let match = null;
|
|
1784
|
+
let ambiguousAt = null;
|
|
1785
|
+
const sig = rowSignature(message);
|
|
1786
|
+
if (sig !== null) {
|
|
1787
|
+
const id = identityKey(message.id);
|
|
1788
|
+
const ts = identityKey(message.timestamp);
|
|
1789
|
+
const exact = [id !== null ? first(byId.get(id), next) : undefined, ts !== null ? first((id !== null ? bySigTsNoId : bySigTs).get(`${sig}\0${ts}`), next) : undefined]
|
|
1790
|
+
.filter((idx) => idx !== undefined);
|
|
1791
|
+
const exactIdx = exact.length ? Math.min(...exact) : null;
|
|
1792
|
+
const weakList = (id !== null ? (ts !== null ? weakNoIdNoTs : weakNoId) : ts !== null ? weakNoTs : weakAny).get(sig) ?? [];
|
|
1793
|
+
const w = lowerBound(weakList, next);
|
|
1794
|
+
const [firstWeak, secondWeak] = [weakList[w], weakList[w + 1]];
|
|
1795
|
+
if (secondWeak !== undefined && (exactIdx === null || secondWeak < exactIdx))
|
|
1796
|
+
ambiguousAt = firstWeak;
|
|
1797
|
+
else
|
|
1798
|
+
match = exactIdx ?? firstWeak ?? null;
|
|
1799
|
+
}
|
|
1800
|
+
matches.push(match);
|
|
1801
|
+
ambiguous.push(ambiguousAt);
|
|
1802
|
+
if (match !== null)
|
|
1803
|
+
next = match + 1;
|
|
1804
|
+
}
|
|
1805
|
+
return { matches, ambiguous };
|
|
1806
|
+
}
|
|
1807
|
+
/** Align model context with the display prefix `full[:keep]` (Python `truncate_context_for_display_keep`, #5096/#5563). */
|
|
1333
1808
|
export function truncateContextForDisplayKeep(context, full, keep) {
|
|
1334
1809
|
if (keep <= 0)
|
|
1335
1810
|
return [];
|
|
1336
|
-
const
|
|
1337
|
-
if (!
|
|
1811
|
+
const ctx = context ?? [];
|
|
1812
|
+
if (!ctx.length || !full.length)
|
|
1338
1813
|
return [];
|
|
1339
|
-
if (keep
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1814
|
+
if (keep < full.length) {
|
|
1815
|
+
const { matches, ambiguous } = alignDisplayToContext(ctx, full);
|
|
1816
|
+
const lastKept = matches[keep - 1];
|
|
1817
|
+
const firstUnkept = matches[keep];
|
|
1818
|
+
const boundary = full[keep - 1];
|
|
1819
|
+
const keptUser = isDict(boundary) && boundary.role === 'user';
|
|
1820
|
+
// Cut at the first unkept display row; a kept user turn ends at its own row so unkept tool rows after it drop.
|
|
1821
|
+
if (firstUnkept !== null)
|
|
1822
|
+
return lastKept !== null && keptUser ? ctx.slice(0, lastKept + 1) : ctx.slice(0, firstUnkept);
|
|
1823
|
+
if (lastKept !== null) {
|
|
1824
|
+
const ambiguousUnkept = ambiguous[keep];
|
|
1825
|
+
return ambiguousUnkept !== null && isDict(boundary) && !keptUser ? ctx.slice(0, ambiguousUnkept) : ctx.slice(0, lastKept + 1);
|
|
1826
|
+
}
|
|
1827
|
+
// Both boundary rows unresolved in a shorter (compressed) context: cut past the last kept display row that resolved,
|
|
1828
|
+
// accepting a weak match. Under-keeping beats slicing at the raw display index.
|
|
1829
|
+
if (ctx.length < full.length) {
|
|
1830
|
+
for (let i = keep - 1; i >= 0; i -= 1) {
|
|
1831
|
+
const resolved = matches[i] ?? ambiguous[i];
|
|
1832
|
+
if (resolved !== null && resolved !== undefined)
|
|
1833
|
+
return ctx.slice(0, resolved + 1);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
// Unalignable (or nothing cut): keep the compaction prefix a longer context carries, then `keep` rows (#5096).
|
|
1838
|
+
const prefixLen = Math.max(0, ctx.length - full.length);
|
|
1839
|
+
return ctx.slice(0, prefixLen + keep);
|
|
1343
1840
|
}
|
|
1344
1841
|
export function truncateSessionAtKeep(session, keep) {
|
|
1345
1842
|
const full = [...session.messages];
|
|
@@ -1350,6 +1847,7 @@ export function truncateSessionAtKeep(session, keep) {
|
|
|
1350
1847
|
session.context_messages = truncateContextForDisplayKeep(session.context_messages, full, keep);
|
|
1351
1848
|
session.truncation_watermark = truncationWatermarkFor(session.messages);
|
|
1352
1849
|
session.truncation_boundary = session.truncation_watermark;
|
|
1850
|
+
session.truncation_watermark_compressed = false;
|
|
1353
1851
|
return [oldMsgCount, oldCtxCount];
|
|
1354
1852
|
}
|
|
1355
1853
|
function shrinkTo(session, lastUserIdx) {
|
|
@@ -1358,6 +1856,7 @@ function shrinkTo(session, lastUserIdx) {
|
|
|
1358
1856
|
stampIntentionalShrink(session, history.length, session.messages.length);
|
|
1359
1857
|
session.truncation_watermark = truncationWatermarkFor(session.messages);
|
|
1360
1858
|
session.truncation_boundary = session.truncation_watermark;
|
|
1859
|
+
session.truncation_watermark_compressed = false;
|
|
1361
1860
|
if (session.context_messages.length) {
|
|
1362
1861
|
const ctxLast = findLastUserIndex(session.context_messages);
|
|
1363
1862
|
if (ctxLast !== null)
|