@maudecode/talaria-web 1.15.0 → 1.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_release.json +4 -4
- package/dist/api/automation-raw.js +6 -3
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +158 -16
- package/dist/api/automation-router.js +12 -6
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +193 -6
- package/dist/api/chat-router.js +39 -16
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +12 -1
- package/dist/api/git-router.js +60 -38
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +6 -0
- package/dist/api/raw-routes.js +35 -58
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +4 -2
- package/dist/api/router.js +8 -8
- package/dist/api/router.js.map +1 -1
- package/dist/api/sessions-router.d.ts +974 -42
- package/dist/api/sessions-router.js +87 -25
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +254 -11
- package/dist/api/settings-router.js +68 -18
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +11 -3
- package/dist/api/sse-routes.js +145 -65
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.js +6 -2
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +86 -9
- package/dist/api/tools-router.js +19 -15
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +0 -8397
- package/dist/app.js +7 -2
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +11 -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.d.ts +2 -0
- package/dist/config/agent-config.js +9 -0
- 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 +6 -3
- package/dist/http/context.js +10 -4
- 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.js +30 -7
- 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 +167 -5
- package/dist/providers/catalog.js +662 -135
- 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.d.ts +1 -0
- package/dist/redact.js +518 -15
- package/dist/redact.js.map +1 -1
- package/dist/runtime.js +79 -37
- package/dist/runtime.js.map +1 -1
- package/dist/sessions/anchor.d.ts +24 -3
- package/dist/sessions/anchor.js +94 -44
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-tasks.d.ts +4 -1
- package/dist/sessions/background-tasks.js +7 -4
- 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/completions.d.ts +13 -1
- package/dist/sessions/completions.js +64 -12
- 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/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 -5
- package/dist/sessions/list.js +56 -26
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +122 -9
- package/dist/sessions/merge.js +544 -33
- 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/service.d.ts +100 -4
- package/dist/sessions/service.js +567 -102
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +16 -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/state-db.d.ts +26 -4
- package/dist/sessions/state-db.js +126 -8
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/store.js +6 -1
- 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/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 +76 -1
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn.d.ts +49 -10
- package/dist/sessions/turn.js +459 -185
- package/dist/sessions/turn.js.map +1 -1
- package/dist/settings.d.ts +20 -2
- package/dist/settings.js +49 -2
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/fake.d.ts +2 -2
- package/dist/sidecar/fake.js +7 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/spa.js +2 -1
- package/dist/spa.js.map +1 -1
- package/dist/test/harness.js +2 -0
- package/dist/test/harness.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 +4 -1
- package/dist/tools/crons.js +58 -11
- package/dist/tools/crons.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 +2 -1
- package/dist/tools/mcp.js +16 -6
- package/dist/tools/mcp.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/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 +15 -3
- package/dist/tools/updates.js +207 -41
- package/dist/tools/updates.js.map +1 -1
- package/dist/workspace/fs.d.ts +7 -3
- package/dist/workspace/fs.js +5 -1
- package/dist/workspace/fs.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 +63 -2
- 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 +5 -4
- package/dist/workspace/rollback.js +12 -9
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/upload.d.ts +2 -0
- package/dist/workspace/upload.js +4 -1
- package/dist/workspace/upload.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 +4 -2
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/__main__.py +7 -1
- package/sidecar/talaria_sidecar/home.py +98 -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 +551 -61
- package/sidecar/talaria_sidecar/methods/commands.py +61 -33
- package/sidecar/talaria_sidecar/methods/config.py +14 -10
- package/sidecar/talaria_sidecar/methods/cron.py +10 -6
- package/sidecar/talaria_sidecar/methods/kanban.py +115 -15
- package/sidecar/talaria_sidecar/methods/oauth.py +439 -0
- package/sidecar/talaria_sidecar/methods/process.py +60 -1
- package/sidecar/talaria_sidecar/methods/profiles.py +12 -5
- package/sidecar/talaria_sidecar/methods/providers.py +161 -1
- package/sidecar/talaria_sidecar/methods/runtime.py +5 -0
- package/sidecar/talaria_sidecar/methods/text.py +18 -21
- package/sidecar/talaria_sidecar/methods/usage.py +264 -5
- package/static/dist/FILES.txt +126 -121
- package/static/dist/assets/AppShell-fb2td9zi.js +1 -0
- package/static/dist/assets/ChatPage-cj96kiy9.js +25 -0
- package/static/dist/assets/Dialog-kxcg7ard.js +1 -0
- package/static/dist/assets/ExtensionRoute-jf7h0754.js +1 -0
- package/static/dist/assets/HubRoute-gmanpd5y.js +2 -0
- package/static/dist/assets/{Markdown-dy8t4dy7.js → Markdown-mdwlvj3w.js} +125 -125
- package/static/dist/assets/RightPanel-ftvrjlcp.js +1 -0
- package/static/dist/assets/Select-bilaypej.js +1 -0
- package/static/dist/assets/SessionListPanel-og8n36qs.js +1 -0
- package/static/dist/assets/SettingsLayout-jonjefmj.js +1 -0
- package/static/dist/assets/States-nmejrwrm.js +1 -0
- package/static/dist/assets/Toaster-nnl8zkt9.js +1 -0
- package/static/dist/assets/_app-gdidzd94.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-opyh9y0v.js +1 -0
- package/static/dist/assets/_app.index-hbarzbjx.js +1 -0
- package/static/dist/assets/_app.insights-luywlarz.js +1 -0
- package/static/dist/assets/_app.kanban-kincnbr8.js +1 -0
- package/static/dist/assets/_app.logs-oi2gr3gs.js +1 -0
- package/static/dist/assets/_app.memory-b6jfivt6.js +1 -0
- package/static/dist/assets/_app.profiles-lpvixmqp.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-gyoh5ylx.js → _app.session._sessionId-bfd4h3id.js} +1 -1
- package/static/dist/assets/_app.settings-vowx3iq0.js +1 -0
- package/static/dist/assets/_app.settings._section-cnntg1tu.js +3 -0
- package/static/dist/assets/_app.skills-fb57lpau.js +1 -0
- package/static/dist/assets/_app.tasks-eszpx1q7.js +2 -0
- package/static/dist/assets/_app.todos-nr05ac6w.js +1 -0
- package/static/dist/assets/_app.workspaces-d2luomu6.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-cjqletra.js → abnfDiagram-VCTEODGH-b2x1lqot.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-hd0o7bde.js +1 -0
- package/static/dist/assets/architectureDiagram-5GKGNRK7-h38o5l7b.js +36 -0
- package/static/dist/assets/{back-e4m6nsnm.js → back-bemaer09.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-b195z021.js → blockDiagram-I7D4REHJ-bqquxv2u.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-olu212k1.js → c4Diagram-7LVT6UL2-f3fb8w6z.js} +1 -1
- package/static/dist/assets/cancel-drd0g6d9.js +1 -0
- package/static/dist/assets/channel-cpdgf867.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-b8o1nufs.js → chunk-4HAMMTFA-bzfyyzp3.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-l1seddto.js → chunk-75Z2AOVW-ly99rfx5.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-lo5zg15v.js → chunk-DU6HZSFF-bg6ecfqf.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-dsdzrp16.js → chunk-F27PBJKO-e8k7mmqw.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-f3fxqq8b.js → chunk-GMAD6QVW-cmalnoaj.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-h8egxtf6.js → chunk-GVQU2GXP-gmif5o01.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-iolwd8s0.js → chunk-IMKFNOWR-f1nwwh6u.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-b88bbtwi.js → chunk-L3NEJ4N5-oktfh5v7.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-e3e7nr6s.js → chunk-OSK3NFVY-b1sdx96z.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-mdc46rt1.js → chunk-P2QGCYS3-fhu4nzxt.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-kqlllrrm.js → chunk-POPQ4Y6H-em31rnef.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-he585k2x.js → chunk-PWAF6VOD-bxhhyftw.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-gh94y9a4.js → chunk-SHT3W25Y-fmrvwcav.js} +3 -3
- package/static/dist/assets/{chunk-SVP7TREG-euxp9y7e.js → chunk-SVP7TREG-lymukl2j.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-c8cxes1q.js → chunk-TICWLB2K-lmza6k23.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-ko09sguk.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-ko09sguk.js +1 -0
- package/static/dist/assets/{copied-kq0u3ht7.js → copied-ima3pysb.js} +1 -1
- package/static/dist/assets/copy-fw5b1pc6.js +1 -0
- package/static/dist/assets/{cynefin-OW5HDTMX-lxd44ux4.js → cynefin-OW5HDTMX-czlhbyhs.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-njpiwat8.js → cynefinDiagram-5FMLGOSQ-gq7mvswb.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-nvgy6yju.js → dagre-GXQ25YYZ-cqdbf7dr.js} +4 -4
- package/static/dist/assets/{diagram-S7CK7UJ4-g98nyv72.js → diagram-S7CK7UJ4-o6arxa58.js} +3 -3
- package/static/dist/assets/{diagram-UQ7AKVKN-ovgm6lvv.js → diagram-UQ7AKVKN-nhnd68tz.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-pb2bn2e4.js → diagram-VSXAHHWV-dk2sclzn.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-6j4mvfqo.js → diagram-VX7I27RA-n3k5nyx3.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-dqmb982r.js → diagram-Z3DM3KII-m6q9cwdh.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-ozbpzb6d.js → ebnfDiagram-PWID7BFC-lsa5641p.js} +1 -1
- package/static/dist/assets/endpoints-d33siokx.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-hl7m8vua.js → erDiagram-RLTQ6QDP-fo2ckbe6.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-emctlo64.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-bfi3moqa.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ompixqmh.js → ganttDiagram-EL5Y4UJY-nzqz6x9g.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-b6vxwp9q.js → gitGraph-4MIJSDKK-gukvcxe6.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-k98f57on.js → gitGraphDiagram-WWUBYQGX-lqhdp8f0.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-cd3ff6ge.js → highlighted-body-KPVGNVTW-igw6cp39.js} +1 -1
- package/static/dist/assets/index-cxq58xb1.js +11 -0
- package/static/dist/assets/index-hgtx08n5.css +2 -0
- package/static/dist/assets/{info-A6RAGUB7-hjcwjami.js → info-A6RAGUB7-gvu7a93l.js} +1 -1
- package/static/dist/assets/infoDiagram-27XIBGKW-m8t8g6w1.js +2 -0
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-bxak4mwu.js → ishikawaDiagram-5VMMS53U-l07vkbgj.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-khmihclr.js → journeyDiagram-3NMN7TZE-hybkgj8l.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-h1r32wdx.js → kanban-definition-UXKFOSKX-by13uvtn.js} +1 -1
- package/static/dist/assets/{lazyRouteComponent-b2eg0fws.js → lazyRouteComponent-af0lvgjx.js} +1 -1
- package/static/dist/assets/{line-friyjof3.js → line-osxq6fq7.js} +1 -1
- package/static/dist/assets/link-jyxicdhg.js +1 -0
- package/static/dist/assets/login-ec21t39x.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-gfl8z3ep.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-f1gvpwrt.js → mermaid-parser.core-i91oxmou.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-mt2oxwue.js → mindmap-definition-YA3MSWOX-m82mx8ww.js} +1 -1
- package/static/dist/assets/onboarding-btnltvyz.js +1 -0
- package/static/dist/assets/onboarding_model_label-dp7xffoh.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-jik4ixlf.js → packet-AYTQ26CC-nbtgge2o.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-erxlv6cc.js → pegDiagram-XKGWAZYB-f1zxzga1.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-mnkoldfl.js → pie-WAS4IAKB-of8vuhxs.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-i7lwrada.js → pieDiagram-E7YTZNPT-o7y8ftio.js} +3 -3
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-fr8ejdoy.js → quadrantDiagram-AXDQQJYC-iqiwia1t.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-otarjte2.js → radar-RG4KPBEZ-delnsy2v.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-lw362oto.js → railroad-74A4TZTK-nspnyq9u.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nst1qxyn.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-ci37x8qy.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-aimkk07z.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-esyn75pw.js → railroadDiagram-O6MQD6OU-z41tqx5z.js} +1 -1
- package/static/dist/assets/refresh-b4uams1i.js +1 -0
- package/static/dist/assets/refresh-cw-nt1dtsx4.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-cyhi5u29.js → requirementDiagram-BXWQKSXE-bdvif194.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-jljpbo0h.js → sankeyDiagram-P5KCCOFB-h73e0rol.js} +1 -1
- package/static/dist/assets/{saved-do49si3v.js → saved-kg52csy7.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-fz0dhlu7.js → sequenceDiagram-WJ2MYXX4-iv95mb1z.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-b7lojn0o.js → settings_section_extensions_title-ej0iapxe.js} +1 -1
- package/static/dist/assets/share._token-f97e94r0.js +2 -0
- package/static/dist/assets/sliders-horizontal-fj18it47.js +1 -0
- package/static/dist/assets/sse-fwpqtzbg.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-kdnithdx.js → stateDiagram-D77RDMKH-k417ixmf.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hwn4apct.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-esd6gmz3.js → swimlanes-42K2YHIH-esola70w.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d1h97oyg.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-g0qygccv.js → timeline-definition-24CTP7MA-getrz2y8.js} +1 -1
- package/static/dist/assets/todoStore-eginf9h4.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-ndchzkuu.js → treeView-Q6P3EWNA-hc7ap1q7.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-hwjxj0zz.js → treemap-WGGIJYW6-b7k1syy5.js} +1 -1
- package/static/dist/assets/tts-lxdxnmjd.js +1 -0
- package/static/dist/assets/{untitled-mbr6tuhu.js → untitled-ovpfthg4.js} +1 -1
- package/static/dist/assets/useForm-bcblv1rx.js +1 -0
- package/static/dist/assets/useLocale-fi5jb8ma.js +1 -0
- package/static/dist/assets/{useParams-lo9u33d1.js → useParams-lhg566ew.js} +1 -1
- package/static/dist/assets/{useSearch-lq1mr090.js → useSearch-lbrh6ejn.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-gubovd1r.js → vennDiagram-4TSXK5OY-n6pcy5qb.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-ir3om65j.js → wardley-WFR3VGLG-lpa18vz1.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-ce0or1ie.js → wardleyDiagram-VM6X3IG4-kpclwwg7.js} +2 -2
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-dexylcjg.js → xychartDiagram-S5SC5T6Z-paa2n8y0.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +2 -2
- package/static/dist/assets/AppShell-orat1s43.js +0 -1
- package/static/dist/assets/ChatPage-ojhka3hy.js +0 -22
- package/static/dist/assets/HubRoute-gxgw1t33.js +0 -2
- package/static/dist/assets/RightPanel-c66ani1s.js +0 -1
- package/static/dist/assets/Select-fswf66up.js +0 -1
- package/static/dist/assets/SessionListPanel-goh5mtt8.js +0 -1
- package/static/dist/assets/SettingsLayout-n5d4wq4m.js +0 -1
- package/static/dist/assets/States-kxyu50at.js +0 -1
- package/static/dist/assets/Toaster-6zmbcqgg.js +0 -1
- package/static/dist/assets/_app-fowyruiu.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-nvjyrwhy.js +0 -1
- package/static/dist/assets/_app.index-c9zv4y7w.js +0 -1
- package/static/dist/assets/_app.insights-jf12nofo.js +0 -1
- package/static/dist/assets/_app.kanban-lri5a4yu.js +0 -1
- package/static/dist/assets/_app.logs-md1p0zqo.js +0 -1
- package/static/dist/assets/_app.memory-bzvf2e28.js +0 -1
- package/static/dist/assets/_app.profiles-jpg7zmbn.js +0 -1
- package/static/dist/assets/_app.settings-mpuwlx4j.js +0 -1
- package/static/dist/assets/_app.settings._section-knz8rwoj.js +0 -1
- package/static/dist/assets/_app.skills-fwu1dkir.js +0 -1
- package/static/dist/assets/_app.tasks-h9gphtl3.js +0 -2
- package/static/dist/assets/_app.todos-hd1dnvrh.js +0 -1
- package/static/dist/assets/_app.workspaces-l8toyn5f.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-cavnusaw.js +0 -1
- package/static/dist/assets/architectureDiagram-5GKGNRK7-k5a7v36n.js +0 -36
- package/static/dist/assets/channel-gozy3dp3.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-n0vs2f2q.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-n0vs2f2q.js +0 -1
- package/static/dist/assets/endpoints-k8agnro8.js +0 -9
- package/static/dist/assets/eventmodeling-NTZA5JFV-j1y2ekec.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-g2cvcbm5.js +0 -1
- package/static/dist/assets/index-nfy0ezs6.css +0 -2
- package/static/dist/assets/index-nqed3oum.js +0 -11
- package/static/dist/assets/infoDiagram-27XIBGKW-fm06rj36.js +0 -2
- package/static/dist/assets/link-m90ahx3n.js +0 -1
- package/static/dist/assets/login-bw7m2fzo.js +0 -1
- package/static/dist/assets/matchContext-f8oy3lo7.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-njwcl62n.js +0 -1
- package/static/dist/assets/onboarding-ret37j1e.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-m741owar.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-noa4fbia.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-o4bpw4nb.js +0 -1
- package/static/dist/assets/refresh-cw-k66keho6.js +0 -1
- package/static/dist/assets/search-j5ki6a9o.js +0 -1
- package/static/dist/assets/share._token-kg8w647b.js +0 -2
- package/static/dist/assets/sliders-horizontal-c4wya4dy.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-k2wg2645.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-n5kvjtgr.js +0 -8
- package/static/dist/assets/tabBuild-bjmyvmn5.js +0 -56
- package/static/dist/assets/todoStore-kjk5l0sy.js +0 -1
- package/static/dist/assets/useForm-ocxu460f.js +0 -1
- package/static/dist/assets/useLocale-fzy5jhg6.js +0 -1
package/dist/sessions/service.js
CHANGED
|
@@ -3,20 +3,24 @@ 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, CLI_IDENTITY_FIELDS, 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, sessionSearchMatches, sessionSearchMessageText, sessionSearchPreview, sessionSearchTerms } from './list.js';
|
|
7
|
+
import { anchorSceneIntOrNull, fullToolResult, hydrateAnchorActivityScenes, normalizeAnchorSceneMessageRef, readAnchorSceneRows, storeAnchorScene, withTurnIds } from './anchor.js';
|
|
8
|
+
import { isSafeSessionId, lastMessageTimestamp, Session, sharePath, stripAttachedFilesMarker, titleFrom } from './session.js';
|
|
9
9
|
import { SessionBusy, SessionNotFound, statSignature } from './store.js';
|
|
10
|
-
import { attachTodoState } from './todo.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
10
|
+
import { UNSETTLED_TODO_KEY, attachTodoState } from './todo.js';
|
|
11
|
+
import { isClaudeCodeSessionId } from './claude-code.js';
|
|
12
|
+
import { stateDbCompressionLineage, stateDbSessionMessages, stateDbSessionRead, stateDbSessionRow, stateDbSessionSources } from './state-db.js';
|
|
13
|
+
import { anchorMessageKey, anchorSummary, CompressionJobs, compressionReference, visibleMessagesForAnchor } from './compress.js';
|
|
13
14
|
import { SidecarError } from '../sidecar/client.js';
|
|
14
|
-
import { attachmentObjects, isContextCompressionMarker, mergeSessionMessagesAppendOnly, pendingUserRow, sanitizeMessagesForApi, withAttachmentObjects, withBodyExcerpts, withPendingUserTurn, withToolCallOutcomes, withoutRunningTurnOutput } from './merge.js';
|
|
15
|
+
import { agentSteerText, attachedFilesPrompt, attachmentObjects, dedupeContext, isContextCompressionMarker, journalOutputRows, looksLikeCurrentUserTurn, stoppedTurnContext, workspaceContextPrefix, mergeSessionMessagesAppendOnly, messageIdentity, pendingUserRow, reasoningFieldsText, sanitizeMessagesForApi, stateDbSeenId, stripWorkspacePrefix, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withSceneRowMedia, withPendingUserTurn, withToolCallOutcomes, withoutRunningTurnOutput } from './merge.js';
|
|
15
16
|
import { withBackgroundUpdates } from './background-updates.js';
|
|
16
17
|
import { withBackgroundLinks } from './background-tasks.js';
|
|
17
18
|
import { messagesForLimitedPayload, messageWindowForDisplay, MAX_MSG_LIMIT, parseMsgLimit, toolCallsForMessageWindow } from './window.js';
|
|
18
19
|
import { redactText } from '../redact.js';
|
|
20
|
+
import { workspaceDisplayName } from '../workspace/workspaces.js';
|
|
19
21
|
import { buildShareSnapshot } from './shares.js';
|
|
22
|
+
import { mediaAnchorRoot, mediaRefPath, mediaTarget } from '../workspace/media.js';
|
|
23
|
+
import { projectMediaRefs } from '../workspace/media-refs.js';
|
|
20
24
|
import { loadGatewaySessionIdentityMap } from './list.js';
|
|
21
25
|
import { join } from 'node:path';
|
|
22
26
|
export class HttpFailure extends Error {
|
|
@@ -47,6 +51,9 @@ export async function ensureAgentRuntimeCurrent(sidecar) {
|
|
|
47
51
|
function staleRuntimeFailure(error) {
|
|
48
52
|
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 } : {}) });
|
|
49
53
|
}
|
|
54
|
+
/** TAL-536: the most of a dead run's journal stale-stream cleanup reads (Python's recovery window). */
|
|
55
|
+
const RECOVERY_JOURNAL_MAX_BYTES = 8 * 1024 * 1024;
|
|
56
|
+
const RECOVERY_JOURNAL_MAX_ROWS = 65536;
|
|
50
57
|
const isDict = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
51
58
|
export class SessionService {
|
|
52
59
|
deps;
|
|
@@ -57,6 +64,37 @@ export class SessionService {
|
|
|
57
64
|
this.compressionJobs = new CompressionJobs();
|
|
58
65
|
}
|
|
59
66
|
get store() { return this.deps.store; }
|
|
67
|
+
/**
|
|
68
|
+
* TAL-186: rewrites a session's media references to their URLs. A local path is rewritten only when `/api/media`
|
|
69
|
+
* serves it for this session, under the same resolution, so every rewritten reference loads.
|
|
70
|
+
*/
|
|
71
|
+
mediaProjector(s) {
|
|
72
|
+
const media = this.deps.media;
|
|
73
|
+
const served = new Map();
|
|
74
|
+
const localUrl = (path) => {
|
|
75
|
+
if (!media?.localIo(s.profile))
|
|
76
|
+
return null;
|
|
77
|
+
if (served.has(path))
|
|
78
|
+
return served.get(path) ?? null;
|
|
79
|
+
let url = null;
|
|
80
|
+
try {
|
|
81
|
+
const target = mediaTarget(mediaRefPath(path));
|
|
82
|
+
if (mediaAnchorRoot(target, s, media.access))
|
|
83
|
+
url = `./api/media?${new URLSearchParams({ path: target, session_id: s.session_id }).toString()}`;
|
|
84
|
+
}
|
|
85
|
+
catch { /* an invalid path stays text */ }
|
|
86
|
+
served.set(path, url);
|
|
87
|
+
return url;
|
|
88
|
+
};
|
|
89
|
+
const workspace = s.workspace.trim() || null;
|
|
90
|
+
// A scene's final answer usually repeats its row's text: each distinct text is parsed once.
|
|
91
|
+
const projected = new Map();
|
|
92
|
+
return (text) => {
|
|
93
|
+
if (!projected.has(text))
|
|
94
|
+
projected.set(text, projectMediaRefs(text, { workspace, localUrl }));
|
|
95
|
+
return projected.get(text) ?? null;
|
|
96
|
+
};
|
|
97
|
+
}
|
|
60
98
|
publish(reason, profile, sessionId) {
|
|
61
99
|
this.deps.events.publish(reason, { profile: profile ?? null, sessionId: sessionId ?? null });
|
|
62
100
|
}
|
|
@@ -151,9 +189,18 @@ export class SessionService {
|
|
|
151
189
|
}
|
|
152
190
|
/** The Agent's state.db rows for this session (empty for subagent views, which never merge). */
|
|
153
191
|
stateDbRows(s) {
|
|
192
|
+
return this.stateDbRead(s).rows;
|
|
193
|
+
}
|
|
194
|
+
/** `stateDbRows` and whether the read succeeded on a state.db whose messages carry ids (TAL-493). */
|
|
195
|
+
stateDbRead(s) {
|
|
154
196
|
if (str(s.source_tag || s.raw_source || s.session_source).trim().toLowerCase() === 'subagent')
|
|
155
|
-
return [];
|
|
156
|
-
|
|
197
|
+
return { rows: [], idCapable: false, ok: false };
|
|
198
|
+
// TAL-529: after a compression rotation the Agent writes to the continuation, so the read follows the recorded lineage.
|
|
199
|
+
const lineage = s.state_db_lineage?.[0] === s.session_id ? s.state_db_lineage : null;
|
|
200
|
+
return stateDbSessionRead(this.stateDbPath(s.profile), s.session_id, { stitch: false, lineage });
|
|
201
|
+
}
|
|
202
|
+
stateDbPath(profile) {
|
|
203
|
+
return join(this.deps.profileHome(profile ?? this.deps.activeProfile()), 'state.db');
|
|
157
204
|
}
|
|
158
205
|
/**
|
|
159
206
|
* Python `_merged_session_messages_for_display`: the sidecar transcript merged append-only with the Agent's state.db
|
|
@@ -163,7 +210,91 @@ export class SessionService {
|
|
|
163
210
|
mergedTranscript(s, local = s.messages, stateRows = this.stateDbRows(s)) {
|
|
164
211
|
if (!stateRows.length)
|
|
165
212
|
return local;
|
|
166
|
-
return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark });
|
|
213
|
+
return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed, stateDbSeenId: currentStateDbSeenId(s, stateRows) });
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* TAL-493: record the highest state.db id this boundary or settled turn read, under the session lock in the same save,
|
|
217
|
+
* so the merge never replays a covered row and appends every row committed after the read. Call it after the boundary
|
|
218
|
+
* fields are set: the marker holds only while they and the row it names stay as recorded.
|
|
219
|
+
*/
|
|
220
|
+
markStateDbSeen(s, read = this.stateDbRead(s), opts = {}) {
|
|
221
|
+
// A failed read says nothing. A settlement keeps the marker; a boundary drops it (fail closed): it may cut rows the
|
|
222
|
+
// marker still admits without moving a field, and the timestamp rules honour its watermark.
|
|
223
|
+
if (!read.ok) {
|
|
224
|
+
if (opts.boundary) {
|
|
225
|
+
s.state_db_seen_id = null;
|
|
226
|
+
s.state_db_seen_boundary = null;
|
|
227
|
+
s.state_db_seen_stamp = null;
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
// A successful read with no rows yet is a baseline: every row the session gets later is new.
|
|
232
|
+
s.state_db_seen_id = stateDbSeenId(read.rows) ?? (read.idCapable ? 0 : null);
|
|
233
|
+
s.state_db_seen_boundary = stateDbMarkKey(s, read.rows, s.state_db_seen_id);
|
|
234
|
+
s.state_db_seen_stamp = s.updated_at;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* TAL-493: a turn's settlement. Each state.db row the merge shows now is matched to what the turn holds: a row already
|
|
238
|
+
* in the context it started from (by id), or one row the Agent added this turn (`agentRows` past that context, matched
|
|
239
|
+
* one to one by full text). An unmatched row is a CLI or gateway row committed while the turn ran: it joins the
|
|
240
|
+
* transcript and the model context before the marker passes it. Without the Agent's rows (`agentRows` empty), rows past
|
|
241
|
+
* `startId` (the highest id the turn read when it started) are its own unreported work: covered, never shown. Rows the
|
|
242
|
+
* turn started from but the transcript lacks (a CLI continuation read into its context) are kept before the turn.
|
|
243
|
+
*/
|
|
244
|
+
settleStateDb(s, turn) {
|
|
245
|
+
const read = this.stateDbRead(s);
|
|
246
|
+
const startedWith = new Set(turn.previousContext.flatMap((m) => (typeof m._state_db_row_id === 'number' ? [m._state_db_row_id] : [])));
|
|
247
|
+
const earlier = mergeSessionMessagesAppendOnly(s.messages, read.rows.filter((m) => typeof m._state_db_row_id === 'number' && startedWith.has(m._state_db_row_id)), { stateDbSeenId: -1 }).slice(s.messages.length);
|
|
248
|
+
const added = new Map();
|
|
249
|
+
for (const m of withoutRows(turn.agentRows ?? [], turn.previousContext))
|
|
250
|
+
added.set(settledIdentity(m), (added.get(settledIdentity(m)) ?? 0) + 1);
|
|
251
|
+
const ownWorkAfter = turn.agentRows?.length ? null : turn.startId ?? null;
|
|
252
|
+
// Without a current marker, the turn's own starting read is the baseline: rows committed after it are judged here.
|
|
253
|
+
const seenId = currentStateDbSeenId(s, read.rows) ?? turn.startId ?? null;
|
|
254
|
+
const shown = read.rows.length ? mergeSessionMessagesAppendOnly(s.messages, read.rows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed, stateDbSeenId: seenId }) : s.messages;
|
|
255
|
+
const fresh = new Set(shown.slice(s.messages.length));
|
|
256
|
+
// The turn's own rows the merge already matched to their local copies use up their share before any shown row can.
|
|
257
|
+
if (seenId !== null) {
|
|
258
|
+
for (const m of read.rows) {
|
|
259
|
+
const id = m._state_db_row_id;
|
|
260
|
+
if (typeof id !== 'number' || id <= seenId || startedWith.has(id) || fresh.has(m))
|
|
261
|
+
continue;
|
|
262
|
+
const left = added.get(settledIdentity(m)) ?? 0;
|
|
263
|
+
if (left > 0)
|
|
264
|
+
added.set(settledIdentity(m), left - 1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const missed = [...fresh].filter((m) => {
|
|
268
|
+
const id = m._state_db_row_id;
|
|
269
|
+
if (typeof id === 'number' && startedWith.has(id))
|
|
270
|
+
return false;
|
|
271
|
+
const key = settledIdentity(m);
|
|
272
|
+
const left = added.get(key) ?? 0;
|
|
273
|
+
if (left > 0) {
|
|
274
|
+
added.set(key, left - 1);
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
return !(ownWorkAfter !== null && typeof id === 'number' && id > ownWorkAfter);
|
|
278
|
+
});
|
|
279
|
+
if (earlier.length) {
|
|
280
|
+
const at = s.messages.findIndex((m) => m._turn_id === turn.turnId);
|
|
281
|
+
s.messages.splice(at < 0 ? s.messages.length : at, 0, ...copyJson(earlier));
|
|
282
|
+
}
|
|
283
|
+
if (missed.length) {
|
|
284
|
+
s.messages.push(...copyJson(missed));
|
|
285
|
+
if (s.context_messages.length)
|
|
286
|
+
s.context_messages.push(...copyJson(missed));
|
|
287
|
+
}
|
|
288
|
+
this.markStateDbSeen(s, read);
|
|
289
|
+
// TAL-529: a compression rotation moved the Agent to a continuation, whose first rows restate the compressed context.
|
|
290
|
+
// The read follows the new lineage from here, and everything it holds now is covered.
|
|
291
|
+
// ponytail: a CLI row committed to the continuation before this settlement is covered with them; telling them apart
|
|
292
|
+
// needs the Agent to mark the rows it writes at rotation.
|
|
293
|
+
const lineage = stateDbCompressionLineage(this.stateDbPath(s.profile), s.session_id);
|
|
294
|
+
if (JSON.stringify(lineage) !== JSON.stringify(s.state_db_lineage ?? [s.session_id])) {
|
|
295
|
+
s.state_db_lineage = lineage.length > 1 ? lineage : null;
|
|
296
|
+
this.markStateDbSeen(s);
|
|
297
|
+
}
|
|
167
298
|
}
|
|
168
299
|
/**
|
|
169
300
|
* Python `reconciled_state_db_messages_for_session(prefer_context=True)`: the model history is the owner context
|
|
@@ -174,15 +305,29 @@ export class SessionService {
|
|
|
174
305
|
const local = s.context_messages.length ? s.context_messages : s.messages.filter((m) => !m._error && !m._partial);
|
|
175
306
|
return local.some((m) => isContextCompressionMarker(m)) ? local : this.mergedTranscript(s, local, stateRows ?? this.stateDbRows(s));
|
|
176
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* TAL-518: the model context a `/btw` clone inherits: the chat's history plus its running turn's prompt, which
|
|
310
|
+
* deferred save keeps out of `messages` and `context_messages` until settlement.
|
|
311
|
+
*/
|
|
312
|
+
sideQuestionContext(s) {
|
|
313
|
+
const context = structuredClone(this.modelContext(s));
|
|
314
|
+
const pending = this.pendingTurn(s);
|
|
315
|
+
return pending ? withPendingUserTurn(context, pending) : context;
|
|
316
|
+
}
|
|
177
317
|
/** Python `_lookup_cli_session_metadata`: the sidebar row for a state.db session in the active profile. */
|
|
178
318
|
lookupCliMeta(sid) {
|
|
179
319
|
try {
|
|
320
|
+
if (isClaudeCodeSessionId(sid))
|
|
321
|
+
return this.claudeCodeRows().find((r) => str(r.session_id) === sid) ?? null;
|
|
180
322
|
return this.deps.cliSessions(this.deps.activeProfile(), { sourceFilter: null }).rows.find((r) => str(r.session_id) === sid) ?? null;
|
|
181
323
|
}
|
|
182
324
|
catch {
|
|
183
325
|
return null;
|
|
184
326
|
}
|
|
185
327
|
}
|
|
328
|
+
claudeCodeRows() {
|
|
329
|
+
return this.deps.claudeCode?.rows(this.deps.workspaces.lastWorkspace(this.deps.activeProfile())) ?? [];
|
|
330
|
+
}
|
|
186
331
|
/** Python `_session_index_marks_was_webui`: an index row that once owned a WebUI sidecar (self-heal 404). */
|
|
187
332
|
indexMarksWasWebui(sid) {
|
|
188
333
|
let entries;
|
|
@@ -219,7 +364,7 @@ export class SessionService {
|
|
|
219
364
|
const subagentChild = stateDbSource === 'subagent';
|
|
220
365
|
if ((this.indexMarksWasWebui(sid) || (this.store.wasDeleted(sid) && ['', 'webui', 'fork'].includes(stateDbSource))) && !subagentChild)
|
|
221
366
|
return { session: null, reason: 'was_webui' };
|
|
222
|
-
const msgs = stateDbSessionMessages(dbPath, sid, { stitch: true });
|
|
367
|
+
const msgs = isClaudeCodeSessionId(sid) ? this.deps.claudeCode?.messages(sid) ?? [] : stateDbSessionMessages(dbPath, sid, { stitch: true });
|
|
223
368
|
if (!msgs.length)
|
|
224
369
|
return { session: null, reason: 'no_foreign_state' };
|
|
225
370
|
const meta = { ...(metaIn ?? this.lookupCliMeta(sid) ?? {}) };
|
|
@@ -266,7 +411,16 @@ export class SessionService {
|
|
|
266
411
|
throw error;
|
|
267
412
|
}
|
|
268
413
|
}
|
|
414
|
+
/**
|
|
415
|
+
* TAL-551: a Claude Code import is a live view of a transcript Claude Code keeps writing. Web never stores a copy,
|
|
416
|
+
* which would freeze its messages and pin it to one profile.
|
|
417
|
+
*/
|
|
418
|
+
rejectClaudeCode(sid, verb) {
|
|
419
|
+
if (isClaudeCodeSessionId(sid))
|
|
420
|
+
throw new HttpFailure(400, `Claude Code sessions are view-only and cannot be ${verb} from WebUI`);
|
|
421
|
+
}
|
|
269
422
|
rejectSubagent(sid, verb) {
|
|
423
|
+
this.rejectClaudeCode(sid, verb);
|
|
270
424
|
if (this.isSubagentViewOnly(sid))
|
|
271
425
|
throw new HttpFailure(400, `Subagent sessions are view-only and cannot be ${verb} from WebUI`);
|
|
272
426
|
}
|
|
@@ -278,13 +432,36 @@ export class SessionService {
|
|
|
278
432
|
publicSession(s, withMessages = true) {
|
|
279
433
|
const payload = this.wireRow(s);
|
|
280
434
|
// Mutation replies replace a client's transcript, so they carry the same server-built scenes as the detail.
|
|
281
|
-
if (withMessages)
|
|
282
|
-
payload.messages = this.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(withBackgroundUpdates(withTurnIds(s.messages), s), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id }));
|
|
435
|
+
if (withMessages) {
|
|
436
|
+
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) }));
|
|
437
|
+
payload.compression_reference = compressionReference(s, payload.messages);
|
|
438
|
+
}
|
|
283
439
|
return redactSessionData(payload, this.deps.redactEnabled());
|
|
284
440
|
}
|
|
285
441
|
/** `compact()` with the wire streaming/read-only flags (TAL-312), for replies that return the session row. */
|
|
286
442
|
wireRow(s) {
|
|
287
|
-
return withSessionWireFlags({ ...s.compact({ contextLengthFor: this.deps.contextLengthFor }), read_only: this.isReadOnly(s), assistant_name: this.assistantName(s) }, this.deps.runtime.activeStreamIds);
|
|
443
|
+
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);
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* TAL-303: the label for a session's workspace, from its own profile's registry (a profileless row is the root's).
|
|
447
|
+
* One resolver serves one response, so each profile's registry is read once however many rows it names.
|
|
448
|
+
*/
|
|
449
|
+
workspaceNames() {
|
|
450
|
+
const registries = new Map();
|
|
451
|
+
return (row) => {
|
|
452
|
+
const profile = str(row.profile) || 'default';
|
|
453
|
+
let entries = registries.get(profile);
|
|
454
|
+
if (!entries) {
|
|
455
|
+
try {
|
|
456
|
+
entries = this.deps.workspaces.entries(profile);
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
entries = [];
|
|
460
|
+
}
|
|
461
|
+
registries.set(profile, entries);
|
|
462
|
+
}
|
|
463
|
+
return workspaceDisplayName(str(row.workspace), entries);
|
|
464
|
+
};
|
|
288
465
|
}
|
|
289
466
|
/** The agent's display name for a session's profile (`assistant_name`). */
|
|
290
467
|
assistantName(s) {
|
|
@@ -328,7 +505,7 @@ export class SessionService {
|
|
|
328
505
|
if (journaled)
|
|
329
506
|
transcript = withoutRunningTurnOutput(transcript, { ...journaled, localCount: s.messages.length });
|
|
330
507
|
// Turn ids, tool outcomes and scenes are computed over the full transcript, so every window reports the same values.
|
|
331
|
-
const all = loadMessages ?
|
|
508
|
+
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 })) : [];
|
|
332
509
|
let truncated = [];
|
|
333
510
|
let offset = 0;
|
|
334
511
|
let summaryCount = null;
|
|
@@ -344,6 +521,8 @@ export class SessionService {
|
|
|
344
521
|
truncated = all.slice(prompt, offset + truncated.length);
|
|
345
522
|
offset = prompt;
|
|
346
523
|
}
|
|
524
|
+
// Per-row display fields (TAL-186 media, TAL-456 excerpts cut from that display text) only for the rows sent.
|
|
525
|
+
truncated = withBodyExcerpts(withDisplayMedia(truncated, this.mediaProjector(s)), s.active_stream_id);
|
|
347
526
|
if (msgLimit !== null)
|
|
348
527
|
truncated = messagesForLimitedPayload(truncated);
|
|
349
528
|
}
|
|
@@ -362,7 +541,7 @@ export class SessionService {
|
|
|
362
541
|
}
|
|
363
542
|
const activeStreamIds = this.deps.runtime.activeStreamIds;
|
|
364
543
|
const raw = {
|
|
365
|
-
...s.compact({ includeRuntime: true, activeStreamIds, contextLengthFor: this.deps.contextLengthFor }),
|
|
544
|
+
...s.compact({ includeRuntime: true, activeStreamIds, contextLengthFor: this.deps.contextLengthFor, modelOptionFor: this.deps.modelOptionFor }),
|
|
366
545
|
messages: truncated,
|
|
367
546
|
message_count: mergedCount,
|
|
368
547
|
tool_calls: toolCalls,
|
|
@@ -376,8 +555,10 @@ export class SessionService {
|
|
|
376
555
|
};
|
|
377
556
|
if (loadMessages)
|
|
378
557
|
raw.transcript_seq = journaled ? { stream_id: journaled.turnId, seq: 0 } : null;
|
|
379
|
-
if (loadMessages
|
|
380
|
-
attachTodoState(raw, all);
|
|
558
|
+
if (loadMessages)
|
|
559
|
+
attachTodoState(raw, all, s.extra[UNSETTLED_TODO_KEY]);
|
|
560
|
+
if (loadMessages)
|
|
561
|
+
raw.compression_reference = compressionReference(s, all);
|
|
381
562
|
if (mergedLast) {
|
|
382
563
|
raw.last_message_at = Math.max(Number(raw.last_message_at ?? 0) || 0, mergedLast);
|
|
383
564
|
raw.updated_at = Math.max(Number(raw.updated_at ?? 0) || 0, mergedLast);
|
|
@@ -390,6 +571,7 @@ export class SessionService {
|
|
|
390
571
|
raw._load_revision = revisionBefore !== null && revisionBefore === revisionAfter ? hashRevision(revisionBefore) : `unstable-${randomUUID().replace(/-/g, '')}`;
|
|
391
572
|
raw.read_only = this.isReadOnly(s);
|
|
392
573
|
raw.assistant_name = this.assistantName(s);
|
|
574
|
+
raw.workspace_name = this.workspaceNames()(s);
|
|
393
575
|
withSessionWireFlags(raw, activeStreamIds);
|
|
394
576
|
raw.pending_steers = raw.active_stream_id ? (this.deps.runtime.pendingSteers?.(str(raw.active_stream_id)) ?? []) : [];
|
|
395
577
|
return redactSessionData(raw, this.deps.redactEnabled());
|
|
@@ -437,7 +619,7 @@ export class SessionService {
|
|
|
437
619
|
foreignSessionDetail(sid) {
|
|
438
620
|
const { synth, meta } = this.foreignSession(sid);
|
|
439
621
|
// The same turn projection as a WebUI session: turn ids, tool outcomes, then each completed turn's scene.
|
|
440
|
-
const msgs = hydrateAnchorActivityScenes(withToolCallOutcomes(withTurnIds(synth.messages), [], null), {});
|
|
622
|
+
const msgs = hydrateAnchorActivityScenes(withToolCallOutcomes(withMarkerKinds(withTurnIds(synth.messages)), [], null), {});
|
|
441
623
|
const lastTs = Number(msgs[msgs.length - 1]?.timestamp ?? 0) || 0;
|
|
442
624
|
const sess = {
|
|
443
625
|
session_id: synth.session_id, title: synth.title, workspace: synth.workspace, model: synth.model, message_count: msgs.length,
|
|
@@ -445,13 +627,13 @@ export class SessionService {
|
|
|
445
627
|
pinned: synth.pinned, archived: synth.archived, project_id: synth.project_id ?? null, profile: synth.profile,
|
|
446
628
|
is_cli_session: synth.is_cli_session, source_tag: synth.source_tag, raw_source: synth.raw_source, session_source: synth.session_source,
|
|
447
629
|
source_label: synth.source_label, read_only: synth.read_only, can_duplicate: false, messages: msgs, tool_calls: [], transcript_seq: null,
|
|
448
|
-
assistant_name: this.assistantName(synth),
|
|
630
|
+
assistant_name: this.assistantName(synth), workspace_name: this.workspaceNames()(synth),
|
|
449
631
|
};
|
|
450
632
|
attachTodoState(sess, msgs);
|
|
451
633
|
const merged = withSessionWireFlags(meta ? mergeCliSidebarMetadata(sess, meta) : sess, this.deps.runtime.activeStreamIds);
|
|
452
634
|
return redactSessionData(merged, this.deps.redactEnabled());
|
|
453
635
|
}
|
|
454
|
-
/** Clear persisted streaming flags when no live stream backs them (Python `_clear_stale_stream_state
|
|
636
|
+
/** Clear persisted streaming flags when no live stream backs them (Python `_clear_stale_stream_state`). */
|
|
455
637
|
clearStaleStreamState(session) {
|
|
456
638
|
const streamId = session.active_stream_id;
|
|
457
639
|
if (!streamId)
|
|
@@ -478,15 +660,40 @@ export class SessionService {
|
|
|
478
660
|
return false;
|
|
479
661
|
}
|
|
480
662
|
}
|
|
481
|
-
// Python `_materialize_pending_user_turn_before_error` (#1361)
|
|
482
|
-
//
|
|
663
|
+
// Python `_materialize_pending_user_turn_before_error` (#1361) and `_recover_dead_run_journal`: the prompt that was in
|
|
664
|
+
// flight becomes a durable user turn, the output its journal holds follows it, and an interruption marker closes a
|
|
665
|
+
// run that never finished with an answer, so a dead stream never silently drops what the user sent or what streamed.
|
|
666
|
+
const turnId = str(target.active_stream_id);
|
|
667
|
+
// ponytail: one bounded tail read (Python's recovery window); a journal past it recovers its latest output only.
|
|
668
|
+
const events = this.deps.journal?.readRunEventTail(target.session_id, turnId, RECOVERY_JOURNAL_MAX_BYTES, RECOVERY_JOURNAL_MAX_ROWS).events ?? [];
|
|
669
|
+
const { rows: output, answered } = journalOutputRows(events, turnId);
|
|
483
670
|
const pendingText = str(target.pending_user_message);
|
|
671
|
+
const attachments = [...target.pending_attachments];
|
|
672
|
+
// The run's starting read died with its worker: the state.db rows from before it started stand in for it. Its own
|
|
673
|
+
// rows open with its prompt and run to the next prompt; anything else another client wrote meanwhile stays.
|
|
674
|
+
const read = this.stateDbRead(target);
|
|
675
|
+
const runStart = target.pending_started_at || events[0]?.created_at || this.deps.now();
|
|
676
|
+
const before = read.rows.filter((m) => Number(m.timestamp) < runStart);
|
|
677
|
+
const ownRows = target.hasPendingPrompt ? runStateDbRows(read.rows.filter((m) => Number(m.timestamp) >= runStart), pendingText) : null;
|
|
678
|
+
// Without this turn's rows: an eager save already put its prompt in the transcript a fresh context falls back to.
|
|
679
|
+
const previousContext = this.modelContext(target, before).filter((m) => m._turn_id !== turnId);
|
|
484
680
|
if (target.hasPendingPrompt) {
|
|
485
|
-
const turnId = str(target.active_stream_id);
|
|
486
681
|
const startedAt = typeof target.pending_started_at === 'number' && target.pending_started_at > 0 ? target.pending_started_at : this.deps.now();
|
|
487
|
-
|
|
488
|
-
target.messages.
|
|
489
|
-
|
|
682
|
+
// An eager save already checkpointed this prompt as the turn's user row.
|
|
683
|
+
if (!target.messages.some((m) => m.role === 'user' && !m._steer && m._turn_id === turnId))
|
|
684
|
+
target.messages.push({ role: 'user', content: pendingText, timestamp: Math.trunc(startedAt), ...(attachments.length ? { attachments } : {}), _recovered: true, _source: target.pending_user_source ?? 'webui', _turn_id: turnId });
|
|
685
|
+
// The model context settles as a Stop's does: the Agent's own rows when it committed the prompt, else the prompt it
|
|
686
|
+
// was sent; then the prose that streamed past them.
|
|
687
|
+
const prompt = attachedFilesPrompt((str(target.workspace) ? workspaceContextPrefix(str(target.workspace)) : '') + pendingText, attachments);
|
|
688
|
+
const streamed = output.map((m) => str(m.content)).filter(Boolean).join('\n\n');
|
|
689
|
+
target.context_messages = dedupeContext(stoppedTurnContext(previousContext, ownRows ? [...previousContext, ...ownRows] : null, prompt, pendingText, streamed, previousContext.length) ?? [...structuredClone(previousContext), { role: 'user', content: prompt }]);
|
|
690
|
+
}
|
|
691
|
+
if (target.hasPendingPrompt || output.length) {
|
|
692
|
+
target.messages.push(...output);
|
|
693
|
+
if (!answered)
|
|
694
|
+
target.messages.push({ role: 'assistant', content: '**Interrupted:** The reply was interrupted before it could be saved.', timestamp: Math.trunc(this.deps.now()), _error: true, _turn_id: turnId });
|
|
695
|
+
// Settled like a turn: the Agent's own rows are covered by the recovered turn; without them, every row past the run's start is.
|
|
696
|
+
this.settleStateDb(target, { turnId, previousContext, agentRows: ownRows, startId: stateDbSeenId(before) ?? (read.idCapable ? 0 : null) });
|
|
490
697
|
}
|
|
491
698
|
target.active_stream_id = null;
|
|
492
699
|
target.pending_user_message = null;
|
|
@@ -515,11 +722,12 @@ export class SessionService {
|
|
|
515
722
|
const wantState = params.showCliSessions || params.showCronSessions || params.showWebhookSessions || params.showKanbanSessions;
|
|
516
723
|
// Python reads every profile's state.db under all_profiles; this port projects the active profile only.
|
|
517
724
|
const cliRead = wantState ? this.deps.cliSessions(activeProfile, { sourceFilter: params.sourceFilter ?? null }) : undefined;
|
|
518
|
-
const
|
|
725
|
+
const claudeCodeRows = params.showCliSessions && params.showClaudeCodeSessions ? this.claudeCodeRows() : [];
|
|
726
|
+
const cliRows = cliRead ? [...cliRead.rows, ...claudeCodeRows] : undefined;
|
|
519
727
|
const gatewayIdentity = loadGatewaySessionIdentityMap(join(this.deps.profileHome(activeProfile), 'sessions', 'sessions.json'));
|
|
520
728
|
const truncatedSources = cliRead?.truncated;
|
|
521
729
|
const payload = buildSessionListPayload(this.store, { ...params, ...(cliRows ? { cliRows } : {}), ...(truncatedSources ? { truncatedSources } : {}), gatewayIdentity, stateDbSources: this.stateDbSources, activeProfile, isolatedProfileMode: this.deps.isolatedProfileMode(), profilesMatch: this.deps.profilesMatch });
|
|
522
|
-
return sessionListResponse(payload, this.deps.runtime, this.deps.redactEnabled(), this.deps.now());
|
|
730
|
+
return sessionListResponse(payload, this.deps.runtime, this.deps.redactEnabled(), this.deps.now(), this.workspaceNames());
|
|
523
731
|
}
|
|
524
732
|
search(q, opts) {
|
|
525
733
|
const activeProfile = this.deps.activeProfile();
|
|
@@ -528,8 +736,10 @@ export class SessionService {
|
|
|
528
736
|
sessions = sessions.filter((r) => this.deps.profilesMatch(str(r.profile) || null, activeProfile));
|
|
529
737
|
sessions = withOwnerLocks(sessions, this.stateDbSources);
|
|
530
738
|
const redact = this.deps.redactEnabled();
|
|
739
|
+
const workspaceName = this.workspaceNames();
|
|
531
740
|
const redactRow = (item) => {
|
|
532
741
|
withSessionWireFlags(item, this.deps.runtime.activeStreamIds);
|
|
742
|
+
item.workspace_name = workspaceName(item);
|
|
533
743
|
if (typeof item.title === 'string')
|
|
534
744
|
item.title = redactText(item.title, redact);
|
|
535
745
|
for (const f of ['display_title', '_state_db_title', 'parent_title'])
|
|
@@ -540,35 +750,71 @@ export class SessionService {
|
|
|
540
750
|
const query = q.toLowerCase().trim();
|
|
541
751
|
if (!query)
|
|
542
752
|
return { sessions: sessions.map((s) => redactRow({ ...s })), all_profiles: opts.allProfiles, active_profile: activeProfile };
|
|
753
|
+
const terms = sessionSearchTerms(query);
|
|
543
754
|
const results = [];
|
|
544
755
|
for (const s of sessions) {
|
|
545
|
-
if (str(s.title)
|
|
756
|
+
if (sessionSearchMatches(str(s.title), terms)) {
|
|
546
757
|
results.push(redactRow({ ...s, match_type: 'title' }));
|
|
547
758
|
continue;
|
|
548
759
|
}
|
|
549
760
|
if (!opts.content)
|
|
550
761
|
continue;
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
762
|
+
const hit = this.contentMatch(str(s.session_id), terms, opts.depth);
|
|
763
|
+
if (hit)
|
|
764
|
+
results.push(redactRow({ ...s, ...hit }));
|
|
765
|
+
}
|
|
766
|
+
return { sessions: results, query, count: results.length, all_profiles: opts.allProfiles, active_profile: activeProfile };
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* TAL-308: the sidebar search. The candidates are exactly the `/api/sessions` rows for `params` (visibility, source,
|
|
770
|
+
* archived, projection and order), narrowed to `projectId` (`none`: rows without a project), then matched on the
|
|
771
|
+
* title, then the metadata the row shows (workspace path and name, model, provider, profile, source label), then message content.
|
|
772
|
+
*/
|
|
773
|
+
sidebarSearch(q, params, opts) {
|
|
774
|
+
let rows = this.list(params).body.sessions;
|
|
775
|
+
if (opts.projectId === 'none')
|
|
776
|
+
rows = rows.filter((r) => !r.project_id);
|
|
777
|
+
else if (opts.projectId)
|
|
778
|
+
rows = rows.filter((r) => str(r.project_id) === opts.projectId);
|
|
779
|
+
const query = q.toLowerCase().trim();
|
|
780
|
+
const base = { all_profiles: params.allProfiles, active_profile: this.deps.activeProfile(), include_archived: params.includeArchived, sidebar_filtered: true };
|
|
781
|
+
if (!query)
|
|
782
|
+
return { ...base, sessions: rows };
|
|
783
|
+
const terms = sessionSearchTerms(query);
|
|
784
|
+
const results = [];
|
|
785
|
+
for (const row of rows) {
|
|
786
|
+
if (sessionSearchMatches(str(row.title), terms))
|
|
787
|
+
results.push({ ...row, match_type: 'title' });
|
|
788
|
+
else if (['workspace', 'workspace_name', 'model', 'model_provider', 'profile', 'source_label'].some((k) => sessionSearchMatches(str(row[k]), terms)))
|
|
789
|
+
results.push({ ...row, match_type: 'metadata' });
|
|
790
|
+
else if (opts.content) {
|
|
791
|
+
const hit = this.contentMatch(str(row.session_id), terms, opts.depth);
|
|
792
|
+
if (hit)
|
|
793
|
+
results.push({ ...row, ...hit });
|
|
554
794
|
}
|
|
555
|
-
|
|
795
|
+
}
|
|
796
|
+
return { ...base, sessions: results, query, count: results.length };
|
|
797
|
+
}
|
|
798
|
+
/** The first of a stored session's first `depth` messages (0: all) containing every term, with its redacted excerpt. */
|
|
799
|
+
contentMatch(sid, terms, depth) {
|
|
800
|
+
let sess;
|
|
801
|
+
try {
|
|
802
|
+
sess = this.store.get(sid, { promote: false, cacheOnMiss: false });
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
for (const m of depth ? sess.messages.slice(0, depth) : sess.messages) {
|
|
808
|
+
const c = sessionSearchMessageText(m);
|
|
809
|
+
if (!sessionSearchMatches(c, terms))
|
|
556
810
|
continue;
|
|
557
|
-
}
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const item = { ...s, match_type: 'content' };
|
|
563
|
-
const preview = sessionSearchPreview(c, query);
|
|
564
|
-
if (preview)
|
|
565
|
-
item.match_preview = redactText(preview, redact);
|
|
566
|
-
results.push(redactRow(item));
|
|
567
|
-
break;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
811
|
+
const item = { match_type: 'content' };
|
|
812
|
+
const preview = sessionSearchPreview(c, terms);
|
|
813
|
+
if (preview)
|
|
814
|
+
item.match_preview = redactText(preview, this.deps.redactEnabled());
|
|
815
|
+
return item;
|
|
570
816
|
}
|
|
571
|
-
return
|
|
817
|
+
return null;
|
|
572
818
|
}
|
|
573
819
|
// ── status / usage ───────────────────────────────────────────────────────
|
|
574
820
|
status(sid) {
|
|
@@ -618,17 +864,20 @@ export class SessionService {
|
|
|
618
864
|
const [workspace] = this.deps.workspaces.resolveImplicitWithRecovery(value, (p) => this.deps.workspaces.profileDefaultWorkspaceForBoot(p), profile);
|
|
619
865
|
return workspace;
|
|
620
866
|
}
|
|
867
|
+
/** The server owns the names' normalization (TAL-631): trimmed, blanks dropped, and nothing left is the profile's defaults. */
|
|
621
868
|
validateToolsetsShape(toolsets) {
|
|
622
869
|
if (toolsets === null || toolsets === undefined)
|
|
623
870
|
return null;
|
|
624
|
-
if (!Array.isArray(toolsets)
|
|
625
|
-
throw new HttpFailure(400, 'toolsets must be a
|
|
626
|
-
if (!toolsets.every((t) => typeof t === 'string'
|
|
627
|
-
throw new HttpFailure(400, 'each toolset must be a
|
|
628
|
-
|
|
871
|
+
if (!Array.isArray(toolsets))
|
|
872
|
+
throw new HttpFailure(400, 'toolsets must be a list or null');
|
|
873
|
+
if (!toolsets.every((t) => typeof t === 'string'))
|
|
874
|
+
throw new HttpFailure(400, 'each toolset must be a string');
|
|
875
|
+
const names = toolsets.map((t) => t.trim()).filter(Boolean);
|
|
876
|
+
return names.length ? names : null;
|
|
629
877
|
}
|
|
630
878
|
create(body, opts = {}) {
|
|
631
|
-
|
|
879
|
+
// Python `new_session`: a body without a profile takes the request's active profile (cookie first).
|
|
880
|
+
const profile = (typeof body.profile === 'string' && body.profile) || this.deps.activeProfile();
|
|
632
881
|
let prevSessionId = typeof body.prev_session_id === 'string' && body.prev_session_id ? body.prev_session_id : null;
|
|
633
882
|
if (prevSessionId && !this.sessionIdVisible(prevSessionId))
|
|
634
883
|
prevSessionId = null;
|
|
@@ -819,6 +1068,7 @@ export class SessionService {
|
|
|
819
1068
|
throw new HttpFailure(400, 'keep_count must be non-negative');
|
|
820
1069
|
await this.store.withLock(sid, () => {
|
|
821
1070
|
truncateSessionAtKeep(s, keep);
|
|
1071
|
+
this.markStateDbSeen(s, undefined, { boundary: true });
|
|
822
1072
|
this.store.save(s);
|
|
823
1073
|
});
|
|
824
1074
|
this.deps.runtime.evictAgent(sid);
|
|
@@ -930,7 +1180,8 @@ export class SessionService {
|
|
|
930
1180
|
if (streamState(live) !== streamBefore)
|
|
931
1181
|
throw new HttpFailure(409, 'Session stream state changed during compression; please retry.');
|
|
932
1182
|
// One state.db read serves the check, the kept display rows, and the boundary.
|
|
933
|
-
const
|
|
1183
|
+
const read = this.stateDbRead(live);
|
|
1184
|
+
const stateRows = read.rows;
|
|
934
1185
|
if (transcriptKey(live, sanitizeMessagesForApi(this.modelContext(live, stateRows))) !== historyKey)
|
|
935
1186
|
throw new HttpFailure(409, 'Session was modified during compression; please retry.');
|
|
936
1187
|
// Rows the sidecar returns without a timestamp take the newest one this compression saw, never the clock: the
|
|
@@ -960,6 +1211,8 @@ export class SessionService {
|
|
|
960
1211
|
// #4836: an intentional-shrink boundary, so append-only state.db reconciliation does not replay compressed rows.
|
|
961
1212
|
live.truncation_watermark = truncationWatermarkFor(compressed);
|
|
962
1213
|
live.truncation_boundary = live.truncation_watermark;
|
|
1214
|
+
live.truncation_watermark_compressed = true;
|
|
1215
|
+
this.markStateDbSeen(live, read, { boundary: true });
|
|
963
1216
|
live.last_prompt_tokens = result.after_tokens;
|
|
964
1217
|
live.post_compression_context_tokens_estimate = result.after_tokens;
|
|
965
1218
|
this.store.save(live);
|
|
@@ -1021,6 +1274,7 @@ export class SessionService {
|
|
|
1021
1274
|
s.pending_user_source = null;
|
|
1022
1275
|
s.clear_generation = hadMessages ? randomUUID().replace(/-/g, '') : null;
|
|
1023
1276
|
applySessionTitleRename(s, 'Untitled');
|
|
1277
|
+
this.markStateDbSeen(s, undefined, { boundary: true });
|
|
1024
1278
|
this.store.save(s);
|
|
1025
1279
|
if (hadMessages) {
|
|
1026
1280
|
try {
|
|
@@ -1029,7 +1283,7 @@ export class SessionService {
|
|
|
1029
1283
|
catch { /* ignore */ }
|
|
1030
1284
|
}
|
|
1031
1285
|
});
|
|
1032
|
-
this.deps.runtime.evictAgent(sid);
|
|
1286
|
+
this.deps.runtime.evictAgent(sid, true);
|
|
1033
1287
|
return { ok: true, session: this.wireRow(s) };
|
|
1034
1288
|
}
|
|
1035
1289
|
async retry(sid) {
|
|
@@ -1040,11 +1294,20 @@ export class SessionService {
|
|
|
1040
1294
|
const lastUser = findLastUserIndex(history);
|
|
1041
1295
|
if (lastUser === null)
|
|
1042
1296
|
return { error: 'No previous message to retry.' };
|
|
1043
|
-
|
|
1297
|
+
// What a client resends (TAL-515): the files that reached the model, which only ever got attachments with a path,
|
|
1298
|
+
// and the prompt as typed. The attached-files line is the server's only when such files exist; otherwise the user
|
|
1299
|
+
// typed it. Nothing to resend fails before the exchange is removed.
|
|
1300
|
+
const prompt = history[lastUser];
|
|
1301
|
+
const lastUserText = extractText(prompt.content);
|
|
1302
|
+
const lastUserAttachments = attachmentObjects(Array.isArray(prompt.attachments) ? prompt.attachments : []).filter((a) => isDict(a) && Boolean(str(a.path).trim()));
|
|
1303
|
+
const lastUserPrompt = stripWorkspacePrefix(lastUserAttachments.length ? stripAttachedFilesMarker(lastUserText) : lastUserText, true);
|
|
1304
|
+
if (!lastUserPrompt && !lastUserAttachments.length)
|
|
1305
|
+
return { error: 'The last message has nothing to resend.' };
|
|
1044
1306
|
const removed = history.length - lastUser;
|
|
1045
1307
|
shrinkTo(s, lastUser);
|
|
1308
|
+
this.markStateDbSeen(s, undefined, { boundary: true });
|
|
1046
1309
|
this.store.save(s);
|
|
1047
|
-
return { ok: true, last_user_text: lastUserText, removed_count: removed };
|
|
1310
|
+
return { ok: true, last_user_text: lastUserText, last_user_prompt: lastUserPrompt, last_user_attachments: lastUserAttachments, removed_count: removed };
|
|
1048
1311
|
});
|
|
1049
1312
|
}
|
|
1050
1313
|
async undo(sid) {
|
|
@@ -1058,6 +1321,7 @@ export class SessionService {
|
|
|
1058
1321
|
const removedText = extractText(history[lastUser]?.content);
|
|
1059
1322
|
const removed = history.length - lastUser;
|
|
1060
1323
|
shrinkTo(s, lastUser);
|
|
1324
|
+
this.markStateDbSeen(s, undefined, { boundary: true });
|
|
1061
1325
|
this.store.save(s);
|
|
1062
1326
|
const preview = removedText.length > 40 ? `${removedText.slice(0, 40)}...` : removedText;
|
|
1063
1327
|
return { ok: true, removed_count: removed, removed_preview: preview };
|
|
@@ -1069,12 +1333,14 @@ export class SessionService {
|
|
|
1069
1333
|
if (!session)
|
|
1070
1334
|
throw new HttpFailure(404, 'Session not found');
|
|
1071
1335
|
const now = this.deps.now();
|
|
1336
|
+
// A truncation cut names deleted state.db rows of the source; the copy's own state.db rows are all genuine (TAL-504).
|
|
1337
|
+
const watermark = session.truncation_watermark_compressed ? { truncation_watermark: session.truncation_watermark, truncation_boundary: session.truncation_boundary, truncation_watermark_compressed: true } : {};
|
|
1072
1338
|
const copied = new Session({
|
|
1073
1339
|
title: `${session.title || 'Untitled'} (copy)`, workspace: session.workspace, model: session.model, model_provider: session.model_provider,
|
|
1074
1340
|
messages: copyJson(session.messages), tool_calls: copyJson(session.tool_calls), pinned: false, archived: false, project_id: session.project_id, profile: session.profile,
|
|
1075
1341
|
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,
|
|
1076
1342
|
personality: session.personality, enabled_toolsets: session.enabled_toolsets, context_length: session.context_length, threshold_tokens: session.threshold_tokens,
|
|
1077
|
-
|
|
1343
|
+
...watermark, context_messages: copyJson(session.context_messages),
|
|
1078
1344
|
gateway_routing: copyJson(session.gateway_routing), gateway_routing_history: copyJson(session.gateway_routing_history), llm_title_generated: session.llm_title_generated,
|
|
1079
1345
|
manual_title: session.manual_title, composer_draft: copyJson(session.composer_draft), context_engine: session.context_engine, context_engine_state: copyJson(session.context_engine_state),
|
|
1080
1346
|
created_at: now, updated_at: now,
|
|
@@ -1131,6 +1397,19 @@ export class SessionService {
|
|
|
1131
1397
|
}
|
|
1132
1398
|
return { session_id: branch.session_id, title, parent_session_id: source.session_id };
|
|
1133
1399
|
}
|
|
1400
|
+
/** The run that keeps `delete` answering 409, if any; stream status reports it so a draining client can wait on it (TAL-622). */
|
|
1401
|
+
activeRunBlocking(sid) {
|
|
1402
|
+
const live = this.deps.runtime.activeRunStream(sid);
|
|
1403
|
+
if (live)
|
|
1404
|
+
return live;
|
|
1405
|
+
try {
|
|
1406
|
+
const candidate = str(this.store.get(sid, { metadataOnly: true, promote: false, cacheOnMiss: false }).active_stream_id).trim();
|
|
1407
|
+
if (candidate && this.deps.runtime.activeStreamIds.has(candidate))
|
|
1408
|
+
return candidate;
|
|
1409
|
+
}
|
|
1410
|
+
catch { /* absent */ }
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1134
1413
|
async delete(sid) {
|
|
1135
1414
|
if (!sid)
|
|
1136
1415
|
throw new HttpFailure(400, 'session_id is required');
|
|
@@ -1163,25 +1442,20 @@ export class SessionService {
|
|
|
1163
1442
|
return true;
|
|
1164
1443
|
}
|
|
1165
1444
|
catch { /* absent */ } const meta = this.lookupCliMeta(sid); return meta !== null && isMessagingSessionRecord(meta); })();
|
|
1166
|
-
|
|
1167
|
-
const live = this.deps.runtime.activeRunStream(sid);
|
|
1168
|
-
if (live)
|
|
1169
|
-
return live;
|
|
1170
|
-
try {
|
|
1171
|
-
const snapshot = this.store.get(sid, { metadataOnly: true });
|
|
1172
|
-
const candidate = str(snapshot.active_stream_id).trim();
|
|
1173
|
-
if (candidate && this.deps.runtime.activeStreamIds.has(candidate))
|
|
1174
|
-
return candidate;
|
|
1175
|
-
}
|
|
1176
|
-
catch { /* absent */ }
|
|
1177
|
-
return null;
|
|
1178
|
-
};
|
|
1179
|
-
if (blocking())
|
|
1445
|
+
if (this.activeRunBlocking(sid))
|
|
1180
1446
|
throw new HttpFailure(409, 'Session has an active run; stop it before deleting');
|
|
1181
1447
|
try {
|
|
1182
1448
|
await this.store.withLock(sid, () => {
|
|
1183
|
-
if (
|
|
1449
|
+
if (this.activeRunBlocking(sid))
|
|
1184
1450
|
throw new HttpFailure(409, 'Session has an active run; stop it before deleting');
|
|
1451
|
+
// A public share outlives its session file, and revokeShare needs the session, so revoke it first.
|
|
1452
|
+
try {
|
|
1453
|
+
this.deps.shares.revoke(this.store.get(sid, { metadataOnly: true }));
|
|
1454
|
+
}
|
|
1455
|
+
catch (error) {
|
|
1456
|
+
if (!(error instanceof SessionNotFound))
|
|
1457
|
+
throw error;
|
|
1458
|
+
}
|
|
1185
1459
|
if (!this.store.deleteFiles(sid))
|
|
1186
1460
|
throw new HttpFailure(500, 'Failed to delete session data');
|
|
1187
1461
|
}, { timeoutMs: 5000 });
|
|
@@ -1191,7 +1465,7 @@ export class SessionService {
|
|
|
1191
1465
|
throw new HttpFailure(503, 'Session busy, try again');
|
|
1192
1466
|
throw error;
|
|
1193
1467
|
}
|
|
1194
|
-
this.deps.runtime.evictAgent(sid);
|
|
1468
|
+
this.deps.runtime.evictAgent(sid, true);
|
|
1195
1469
|
try {
|
|
1196
1470
|
rmSync(this.deps.attachmentDir(sid), { recursive: true, force: true });
|
|
1197
1471
|
}
|
|
@@ -1206,11 +1480,15 @@ export class SessionService {
|
|
|
1206
1480
|
// messaging channel's memory is never erased from the WebUI; the actual outcome is reported.
|
|
1207
1481
|
let stateDbCleanupFailed = false;
|
|
1208
1482
|
if (!isMessaging) {
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1483
|
+
// TAL-529: the compression continuations go too, tip first; the Agent's delete keeps and detaches them otherwise.
|
|
1484
|
+
for (const id of stateDbCompressionLineage(this.stateDbPath(eventProfile), sid).reverse()) {
|
|
1485
|
+
try {
|
|
1486
|
+
if (!(await this.deps.runtime.deleteCliSession(eventProfile, id)))
|
|
1487
|
+
stateDbCleanupFailed = true;
|
|
1488
|
+
}
|
|
1489
|
+
catch {
|
|
1490
|
+
stateDbCleanupFailed = true;
|
|
1491
|
+
}
|
|
1214
1492
|
}
|
|
1215
1493
|
}
|
|
1216
1494
|
this.publish('session_delete', eventProfile);
|
|
@@ -1226,6 +1504,13 @@ export class SessionService {
|
|
|
1226
1504
|
const shouldDelete = zeroOnly ? s.messages.length === 0 : s.title === 'Untitled' && s.messages.length === 0;
|
|
1227
1505
|
if (!shouldDelete)
|
|
1228
1506
|
continue;
|
|
1507
|
+
// A cleared session keeps its share; keep the session (and its revoke route) if the share cannot be revoked.
|
|
1508
|
+
try {
|
|
1509
|
+
this.deps.shares.revoke(s);
|
|
1510
|
+
}
|
|
1511
|
+
catch {
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1229
1514
|
this.store.sessions.delete(s.session_id);
|
|
1230
1515
|
rmSync(this.store.pathFor(s.session_id), { force: true });
|
|
1231
1516
|
cleaned += 1;
|
|
@@ -1426,39 +1711,54 @@ export class SessionService {
|
|
|
1426
1711
|
return { ok: true, ...placement };
|
|
1427
1712
|
});
|
|
1428
1713
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
const messageRef = normalizeAnchorSceneMessageRef(query.message_ref);
|
|
1432
|
-
const messageIndex = anchorSceneIntOrNull(query.message_index);
|
|
1433
|
-
if (!sid || (!messageRef && messageIndex === null))
|
|
1434
|
-
throw new HttpFailure(400, 'session_id and message_ref or message_index are required');
|
|
1435
|
-
let session;
|
|
1436
|
-
let transcript;
|
|
1714
|
+
/** The session and the full transcript its detail is built from; a state.db-only session gives its synthesized one. */
|
|
1715
|
+
fullTranscript(sid) {
|
|
1437
1716
|
try {
|
|
1438
|
-
session = this.store.get(sid);
|
|
1717
|
+
let session = this.store.get(sid);
|
|
1439
1718
|
if (session.loadedMetadataOnly)
|
|
1440
1719
|
session = this.store.load(sid) ?? session;
|
|
1441
1720
|
if (!this.visibleToActiveProfile(session.profile))
|
|
1442
1721
|
throw new HttpFailure(404, 'Session not found');
|
|
1443
|
-
transcript
|
|
1722
|
+
return { session, transcript: this.mergedTranscript(session), stored: true };
|
|
1444
1723
|
}
|
|
1445
1724
|
catch (error) {
|
|
1446
1725
|
if (error instanceof HttpFailure)
|
|
1447
1726
|
throw error;
|
|
1448
|
-
|
|
1449
|
-
session
|
|
1450
|
-
transcript = session.messages;
|
|
1727
|
+
const session = this.foreignSession(sid).synth;
|
|
1728
|
+
return { session, transcript: session.messages, stored: false };
|
|
1451
1729
|
}
|
|
1730
|
+
}
|
|
1731
|
+
readAnchorScene(query) {
|
|
1732
|
+
const sid = str(query.session_id).trim();
|
|
1733
|
+
const messageRef = normalizeAnchorSceneMessageRef(query.message_ref);
|
|
1734
|
+
const messageIndex = anchorSceneIntOrNull(query.message_index);
|
|
1735
|
+
if (!sid || (!messageRef && messageIndex === null))
|
|
1736
|
+
throw new HttpFailure(400, 'session_id and message_ref or message_index are required');
|
|
1737
|
+
const { session, transcript, stored } = this.fullTranscript(sid);
|
|
1452
1738
|
const result = readAnchorSceneRows(session, { messageRef, messageIndex, before: anchorSceneIntOrNull(query.before), limit: anchorSceneIntOrNull(query.limit) }, withToolCallOutcomes(withTurnIds(transcript), session.tool_calls, session.active_stream_id));
|
|
1453
1739
|
if (!result)
|
|
1454
1740
|
throw new HttpFailure(404, 'Anchor activity scene not found');
|
|
1455
1741
|
// Paged rows come from the raw transcript, so they take the same credential redaction as the detail's preview.
|
|
1456
1742
|
const enabled = this.deps.redactEnabled();
|
|
1457
1743
|
const redacted = redactValue(result, enabled);
|
|
1458
|
-
|
|
1744
|
+
const rows = withSceneToolDisplay(result.rows, redacted.rows, enabled);
|
|
1745
|
+
// `/api/media` serves stored sessions only, so a state.db-only session's rows stay as written, as its detail does.
|
|
1746
|
+
return { ...redacted, rows: stored ? withSceneRowMedia(rows, this.mediaProjector(session)) : rows };
|
|
1747
|
+
}
|
|
1748
|
+
/** TAL-331: one tool call's whole result, which a limited response clipped (`result_truncated`), redacted like the detail. */
|
|
1749
|
+
readToolResult(query) {
|
|
1750
|
+
const sid = query.session_id.trim();
|
|
1751
|
+
const id = query.tool_call_id.trim();
|
|
1752
|
+
if (!sid || !id)
|
|
1753
|
+
throw new HttpFailure(400, 'session_id and tool_call_id are required');
|
|
1754
|
+
const full = fullToolResult(this.fullTranscript(sid).transcript, id);
|
|
1755
|
+
if (full === null)
|
|
1756
|
+
throw new HttpFailure(404, 'Tool result not found');
|
|
1757
|
+
return { tool_call_id: id, ...redactValue(full, this.deps.redactEnabled()) };
|
|
1459
1758
|
}
|
|
1460
1759
|
// ── shares ───────────────────────────────────────────────────────────────
|
|
1461
1760
|
createShare(sid) {
|
|
1761
|
+
this.rejectClaudeCode(sid, 'shared');
|
|
1462
1762
|
let s = this.get404(sid);
|
|
1463
1763
|
if (!this.visibleToActiveProfile(s.profile))
|
|
1464
1764
|
throw new HttpFailure(404, 'Session not found');
|
|
@@ -1477,7 +1777,7 @@ export class SessionService {
|
|
|
1477
1777
|
this.publish('session_share_create', s.profile, sid);
|
|
1478
1778
|
return {
|
|
1479
1779
|
ok: true,
|
|
1480
|
-
share: { token: meta.share_token, url:
|
|
1780
|
+
share: { token: meta.share_token, url: sharePath(meta.share_token) ?? '', title: meta.share_title, message_count: meta.share_message_count, created_at: meta.share_created_at, updated_at: meta.share_updated_at },
|
|
1481
1781
|
session: this.publicSession(s),
|
|
1482
1782
|
};
|
|
1483
1783
|
}
|
|
@@ -1544,6 +1844,44 @@ function findLastUserIndex(history) {
|
|
|
1544
1844
|
export function sanitizePaths(error) {
|
|
1545
1845
|
return str(error?.message ?? error).replace(/(?:(?:\/[a-zA-Z0-9_.-]+)+|(?:[A-Z]:\\[^\s]+))/g, '<path>');
|
|
1546
1846
|
}
|
|
1847
|
+
/** TAL-536: a dead run's own state.db rows: from its prompt to the next prompt (tool results and applied steers stay in it). */
|
|
1848
|
+
function runStateDbRows(rows, prompt) {
|
|
1849
|
+
const at = rows.findIndex((m) => looksLikeCurrentUserTurn(m, prompt));
|
|
1850
|
+
if (at < 0)
|
|
1851
|
+
return null;
|
|
1852
|
+
const toolResult = (m) => Array.isArray(m.content) && m.content.some((part) => isDict(part) && part.type === 'tool_result');
|
|
1853
|
+
const end = rows.findIndex((m, i) => i > at && m.role === 'user' && !toolResult(m) && agentSteerText(m) === null);
|
|
1854
|
+
return rows.slice(at, end < 0 ? undefined : end);
|
|
1855
|
+
}
|
|
1856
|
+
/** TAL-493: a row's full-text identity; one with no text (reasoning or media only) by its raw role, content, and reasoning. */
|
|
1857
|
+
function settledIdentity(m) {
|
|
1858
|
+
return messageIdentity(m, Infinity) ?? JSON.stringify([m.role ?? null, m.content ?? null, reasoningFieldsText(m)]);
|
|
1859
|
+
}
|
|
1860
|
+
/** `rows` minus one occurrence of each row of `base`, by `settledIdentity`. */
|
|
1861
|
+
function withoutRows(rows, base) {
|
|
1862
|
+
const left = new Map();
|
|
1863
|
+
for (const m of base)
|
|
1864
|
+
left.set(settledIdentity(m), (left.get(settledIdentity(m)) ?? 0) + 1);
|
|
1865
|
+
return rows.filter((m) => { const key = settledIdentity(m); const n = left.get(key) ?? 0; if (n > 0)
|
|
1866
|
+
left.set(key, n - 1); return n === 0; });
|
|
1867
|
+
}
|
|
1868
|
+
/**
|
|
1869
|
+
* TAL-493: what the state.db marker was recorded under: the boundary fields every boundary writer sets (an older release
|
|
1870
|
+
* moves them without the marker) and the role, timestamp, and database file identity of the row the marker names (a
|
|
1871
|
+
* recreated state.db, whose ids start over, has no such row).
|
|
1872
|
+
*/
|
|
1873
|
+
function stateDbMarkKey(s, stateRows, seenId) {
|
|
1874
|
+
const anchor = seenId === null ? undefined : stateRows.find((m) => m._state_db_row_id === seenId);
|
|
1875
|
+
return JSON.stringify([s.truncation_watermark ?? null, s.truncation_boundary ?? null, s.intentional_shrink_generation ?? null, s.clear_generation ?? null, anchor ? [anchor.role ?? null, anchor.timestamp ?? null, anchor._state_db_generation ?? null] : null]);
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* The recorded state.db marker while what it was recorded under still holds and this version wrote the session last (an
|
|
1879
|
+
* older release can apply a boundary that changes no field), else null (the timestamp rules apply).
|
|
1880
|
+
*/
|
|
1881
|
+
function currentStateDbSeenId(s, stateRows) {
|
|
1882
|
+
const seenId = s.state_db_seen_id;
|
|
1883
|
+
return seenId !== null && s.state_db_seen_stamp === s.updated_at && s.state_db_seen_boundary === stateDbMarkKey(s, stateRows, seenId) ? seenId : null;
|
|
1884
|
+
}
|
|
1547
1885
|
function truncationWatermarkFor(messages) {
|
|
1548
1886
|
if (!messages.length)
|
|
1549
1887
|
return 0.0;
|
|
@@ -1557,17 +1895,142 @@ function stampIntentionalShrink(session, oldCount, newCount) {
|
|
|
1557
1895
|
session.intentional_shrink_generation = randomUUID().replace(/-/g, '');
|
|
1558
1896
|
return true;
|
|
1559
1897
|
}
|
|
1560
|
-
/**
|
|
1898
|
+
/** Python truthiness for the JSON values a transcript row carries. */
|
|
1899
|
+
function pyTruthy(v) {
|
|
1900
|
+
if (Array.isArray(v))
|
|
1901
|
+
return v.length > 0;
|
|
1902
|
+
if (isDict(v))
|
|
1903
|
+
return Object.keys(v).length > 0;
|
|
1904
|
+
return Boolean(v);
|
|
1905
|
+
}
|
|
1906
|
+
/** JSON with sorted object keys (Python `json.dumps(..., sort_keys=True)`). */
|
|
1907
|
+
function sortedJson(v) {
|
|
1908
|
+
return JSON.stringify(v, (_key, value) => (isDict(value) ? Object.fromEntries(Object.keys(value).sort().map((k) => [k, value[k]])) : value)) ?? '';
|
|
1909
|
+
}
|
|
1910
|
+
/** Equality key for a row `id`/`timestamp`; `null` when the value is absent. Numbers compare by value (`2 == 2.0`). */
|
|
1911
|
+
function identityKey(v) {
|
|
1912
|
+
if (v === null || v === undefined)
|
|
1913
|
+
return null;
|
|
1914
|
+
return typeof v === 'string' ? `s${v}` : typeof v === 'number' ? `n${String(v)}` : `j${sortedJson(v)}`;
|
|
1915
|
+
}
|
|
1916
|
+
function rowSignature(row) {
|
|
1917
|
+
if (!isDict(row))
|
|
1918
|
+
return null;
|
|
1919
|
+
const field = (v) => (!pyTruthy(v) ? '' : typeof v === 'string' ? v : sortedJson(v));
|
|
1920
|
+
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)]);
|
|
1921
|
+
}
|
|
1922
|
+
/** First index in sorted `positions` whose value is `>= start`. */
|
|
1923
|
+
function lowerBound(positions, start) {
|
|
1924
|
+
let lo = 0;
|
|
1925
|
+
let hi = positions.length;
|
|
1926
|
+
while (lo < hi) {
|
|
1927
|
+
const mid = (lo + hi) >> 1;
|
|
1928
|
+
if (positions[mid] < start)
|
|
1929
|
+
lo = mid + 1;
|
|
1930
|
+
else
|
|
1931
|
+
hi = mid;
|
|
1932
|
+
}
|
|
1933
|
+
return lo;
|
|
1934
|
+
}
|
|
1935
|
+
/**
|
|
1936
|
+
* Map each display row to the context row it represents, scanning forward. An `id` match wins; otherwise the role+content
|
|
1937
|
+
* signature must match, and a shared `timestamp` makes it exact. A signature match where an identity is missing is weak:
|
|
1938
|
+
* one weak candidate counts as a match, a second one before any exact match leaves the row ambiguous.
|
|
1939
|
+
*/
|
|
1940
|
+
function alignDisplayToContext(ctx, full) {
|
|
1941
|
+
const index = () => new Map();
|
|
1942
|
+
const push = (map, key, idx) => {
|
|
1943
|
+
const list = map.get(key);
|
|
1944
|
+
if (list)
|
|
1945
|
+
list.push(idx);
|
|
1946
|
+
else
|
|
1947
|
+
map.set(key, [idx]);
|
|
1948
|
+
};
|
|
1949
|
+
const first = (list, start) => list?.[lowerBound(list, start)];
|
|
1950
|
+
const byId = index(), bySigTs = index(), bySigTsNoId = index();
|
|
1951
|
+
const weakAny = index(), weakNoId = index(), weakNoTs = index(), weakNoIdNoTs = index();
|
|
1952
|
+
ctx.forEach((row, idx) => {
|
|
1953
|
+
const sig = rowSignature(row);
|
|
1954
|
+
if (sig === null)
|
|
1955
|
+
return;
|
|
1956
|
+
const id = identityKey(row.id);
|
|
1957
|
+
const ts = identityKey(row.timestamp);
|
|
1958
|
+
push(weakAny, sig, idx);
|
|
1959
|
+
if (id === null)
|
|
1960
|
+
push(weakNoId, sig, idx);
|
|
1961
|
+
if (ts === null)
|
|
1962
|
+
push(weakNoTs, sig, idx);
|
|
1963
|
+
if (id === null && ts === null)
|
|
1964
|
+
push(weakNoIdNoTs, sig, idx);
|
|
1965
|
+
if (id !== null)
|
|
1966
|
+
push(byId, id, idx);
|
|
1967
|
+
if (ts !== null) {
|
|
1968
|
+
push(bySigTs, `${sig}\0${ts}`, idx);
|
|
1969
|
+
if (id === null)
|
|
1970
|
+
push(bySigTsNoId, `${sig}\0${ts}`, idx);
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
const matches = [];
|
|
1974
|
+
const ambiguous = [];
|
|
1975
|
+
let next = 0;
|
|
1976
|
+
for (const message of full) {
|
|
1977
|
+
let match = null;
|
|
1978
|
+
let ambiguousAt = null;
|
|
1979
|
+
const sig = rowSignature(message);
|
|
1980
|
+
if (sig !== null) {
|
|
1981
|
+
const id = identityKey(message.id);
|
|
1982
|
+
const ts = identityKey(message.timestamp);
|
|
1983
|
+
const exact = [id !== null ? first(byId.get(id), next) : undefined, ts !== null ? first((id !== null ? bySigTsNoId : bySigTs).get(`${sig}\0${ts}`), next) : undefined]
|
|
1984
|
+
.filter((idx) => idx !== undefined);
|
|
1985
|
+
const exactIdx = exact.length ? Math.min(...exact) : null;
|
|
1986
|
+
const weakList = (id !== null ? (ts !== null ? weakNoIdNoTs : weakNoId) : ts !== null ? weakNoTs : weakAny).get(sig) ?? [];
|
|
1987
|
+
const w = lowerBound(weakList, next);
|
|
1988
|
+
const [firstWeak, secondWeak] = [weakList[w], weakList[w + 1]];
|
|
1989
|
+
if (secondWeak !== undefined && (exactIdx === null || secondWeak < exactIdx))
|
|
1990
|
+
ambiguousAt = firstWeak;
|
|
1991
|
+
else
|
|
1992
|
+
match = exactIdx ?? firstWeak ?? null;
|
|
1993
|
+
}
|
|
1994
|
+
matches.push(match);
|
|
1995
|
+
ambiguous.push(ambiguousAt);
|
|
1996
|
+
if (match !== null)
|
|
1997
|
+
next = match + 1;
|
|
1998
|
+
}
|
|
1999
|
+
return { matches, ambiguous };
|
|
2000
|
+
}
|
|
2001
|
+
/** Align model context with the display prefix `full[:keep]` (Python `truncate_context_for_display_keep`, #5096/#5563). */
|
|
1561
2002
|
export function truncateContextForDisplayKeep(context, full, keep) {
|
|
1562
2003
|
if (keep <= 0)
|
|
1563
2004
|
return [];
|
|
1564
|
-
const
|
|
1565
|
-
if (!
|
|
2005
|
+
const ctx = context ?? [];
|
|
2006
|
+
if (!ctx.length || !full.length)
|
|
1566
2007
|
return [];
|
|
1567
|
-
if (keep
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
2008
|
+
if (keep < full.length) {
|
|
2009
|
+
const { matches, ambiguous } = alignDisplayToContext(ctx, full);
|
|
2010
|
+
const lastKept = matches[keep - 1];
|
|
2011
|
+
const firstUnkept = matches[keep];
|
|
2012
|
+
const boundary = full[keep - 1];
|
|
2013
|
+
const keptUser = isDict(boundary) && boundary.role === 'user';
|
|
2014
|
+
// Cut at the first unkept display row; a kept user turn ends at its own row so unkept tool rows after it drop.
|
|
2015
|
+
if (firstUnkept !== null)
|
|
2016
|
+
return lastKept !== null && keptUser ? ctx.slice(0, lastKept + 1) : ctx.slice(0, firstUnkept);
|
|
2017
|
+
if (lastKept !== null) {
|
|
2018
|
+
const ambiguousUnkept = ambiguous[keep];
|
|
2019
|
+
return ambiguousUnkept !== null && isDict(boundary) && !keptUser ? ctx.slice(0, ambiguousUnkept) : ctx.slice(0, lastKept + 1);
|
|
2020
|
+
}
|
|
2021
|
+
// Both boundary rows unresolved in a shorter (compressed) context: cut past the last kept display row that resolved,
|
|
2022
|
+
// accepting a weak match. Under-keeping beats slicing at the raw display index.
|
|
2023
|
+
if (ctx.length < full.length) {
|
|
2024
|
+
for (let i = keep - 1; i >= 0; i -= 1) {
|
|
2025
|
+
const resolved = matches[i] ?? ambiguous[i];
|
|
2026
|
+
if (resolved !== null && resolved !== undefined)
|
|
2027
|
+
return ctx.slice(0, resolved + 1);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
// Unalignable (or nothing cut): keep the compaction prefix a longer context carries, then `keep` rows (#5096).
|
|
2032
|
+
const prefixLen = Math.max(0, ctx.length - full.length);
|
|
2033
|
+
return ctx.slice(0, prefixLen + keep);
|
|
1571
2034
|
}
|
|
1572
2035
|
export function truncateSessionAtKeep(session, keep) {
|
|
1573
2036
|
const full = [...session.messages];
|
|
@@ -1578,6 +2041,7 @@ export function truncateSessionAtKeep(session, keep) {
|
|
|
1578
2041
|
session.context_messages = truncateContextForDisplayKeep(session.context_messages, full, keep);
|
|
1579
2042
|
session.truncation_watermark = truncationWatermarkFor(session.messages);
|
|
1580
2043
|
session.truncation_boundary = session.truncation_watermark;
|
|
2044
|
+
session.truncation_watermark_compressed = false;
|
|
1581
2045
|
return [oldMsgCount, oldCtxCount];
|
|
1582
2046
|
}
|
|
1583
2047
|
function shrinkTo(session, lastUserIdx) {
|
|
@@ -1586,6 +2050,7 @@ function shrinkTo(session, lastUserIdx) {
|
|
|
1586
2050
|
stampIntentionalShrink(session, history.length, session.messages.length);
|
|
1587
2051
|
session.truncation_watermark = truncationWatermarkFor(session.messages);
|
|
1588
2052
|
session.truncation_boundary = session.truncation_watermark;
|
|
2053
|
+
session.truncation_watermark_compressed = false;
|
|
1589
2054
|
if (session.context_messages.length) {
|
|
1590
2055
|
const ctxLast = findLastUserIndex(session.context_messages);
|
|
1591
2056
|
if (ctxLast !== null)
|