@maudecode/talaria-web 1.16.1 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_release.json +8 -8
- package/dist/api/auth-raw.d.ts +1 -1
- package/dist/api/auth-raw.js +4 -4
- package/dist/api/auth-raw.js.map +1 -1
- package/dist/api/automation-raw.d.ts +6 -0
- package/dist/api/automation-raw.js +59 -9
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +163 -10
- package/dist/api/automation-router.js +25 -8
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +700 -15
- package/dist/api/chat-router.js +74 -55
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +47 -2
- package/dist/api/git-router.js +31 -22
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +14 -0
- package/dist/api/raw-routes.js +277 -36
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +10 -0
- package/dist/api/router.js +10 -8
- package/dist/api/router.js.map +1 -1
- package/dist/api/sessions-router.d.ts +1710 -73
- package/dist/api/sessions-router.js +164 -10
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +132 -2
- package/dist/api/settings-router.js +15 -5
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +16 -3
- package/dist/api/sse-routes.js +194 -67
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.js +12 -25
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +425 -19
- package/dist/api/tools-router.js +157 -26
- package/dist/api/tools-router.js.map +1 -1
- package/dist/app.d.ts +0 -9377
- package/dist/app.js +42 -17
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +8 -2
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/oidc.d.ts +5 -2
- package/dist/auth/oidc.js +8 -7
- package/dist/auth/oidc.js.map +1 -1
- package/dist/bin/talaria-web.js +1 -1
- package/dist/bin/talaria-web.js.map +1 -1
- package/dist/cli/dotenv.js +1 -0
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/config/agent-config.d.ts +2 -0
- package/dist/config/agent-config.js +12 -2
- package/dist/config/agent-config.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/http/context.d.ts +20 -3
- package/dist/http/context.js +41 -8
- package/dist/http/context.js.map +1 -1
- package/dist/http/csp.d.ts +2 -1
- package/dist/http/csp.js +3 -2
- package/dist/http/csp.js.map +1 -1
- package/dist/onboarding.js +4 -0
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.d.ts +10 -0
- package/dist/profiles/profiles.js +40 -2
- package/dist/profiles/profiles.js.map +1 -1
- package/dist/providers/catalog.d.ts +101 -8
- package/dist/providers/catalog.js +463 -123
- package/dist/providers/catalog.js.map +1 -1
- package/dist/raster-image.d.ts +7 -0
- package/dist/raster-image.js +267 -0
- package/dist/raster-image.js.map +1 -0
- package/dist/redact.d.ts +1 -0
- package/dist/redact.js +354 -44
- package/dist/redact.js.map +1 -1
- package/dist/runtime.js +111 -27
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +5 -0
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +2 -0
- package/dist/sessions/anchor.js +8 -3
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-tasks.d.ts +2 -9
- package/dist/sessions/background-tasks.js +2 -12
- package/dist/sessions/background-tasks.js.map +1 -1
- package/dist/sessions/claude-code.d.ts +19 -0
- package/dist/sessions/claude-code.js +250 -0
- package/dist/sessions/claude-code.js.map +1 -0
- package/dist/sessions/cli-sessions.d.ts +15 -4
- package/dist/sessions/cli-sessions.js +120 -29
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +61 -9
- package/dist/sessions/completions.js +282 -34
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/compress.d.ts +14 -1
- package/dist/sessions/compress.js +52 -11
- package/dist/sessions/compress.js.map +1 -1
- package/dist/sessions/export.d.ts +5 -0
- package/dist/sessions/export.js +30 -0
- package/dist/sessions/export.js.map +1 -1
- package/dist/sessions/gateway-watcher.d.ts +7 -3
- package/dist/sessions/gateway-watcher.js +31 -19
- package/dist/sessions/gateway-watcher.js.map +1 -1
- package/dist/sessions/handoff.d.ts +32 -0
- package/dist/sessions/handoff.js +114 -0
- package/dist/sessions/handoff.js.map +1 -0
- package/dist/sessions/journal.d.ts +8 -0
- package/dist/sessions/journal.js +44 -10
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +13 -3
- package/dist/sessions/list.js +46 -15
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +53 -5
- package/dist/sessions/merge.js +219 -23
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/recovery.d.ts +22 -0
- package/dist/sessions/recovery.js +506 -0
- package/dist/sessions/recovery.js.map +1 -0
- package/dist/sessions/relay.d.ts +21 -2
- package/dist/sessions/relay.js +156 -11
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +152 -8
- package/dist/sessions/service.js +640 -72
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +17 -1
- package/dist/sessions/session.js +63 -4
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/state-db-worker.d.ts +1 -0
- package/dist/sessions/state-db-worker.js +33 -0
- package/dist/sessions/state-db-worker.js.map +1 -0
- package/dist/sessions/state-db.d.ts +87 -4
- package/dist/sessions/state-db.js +417 -8
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/store.d.ts +7 -0
- package/dist/sessions/store.js +36 -22
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +10 -0
- package/dist/sessions/streams.js +14 -3
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/titles.d.ts +2 -0
- package/dist/sessions/titles.js +4 -0
- package/dist/sessions/titles.js.map +1 -1
- package/dist/sessions/todo.d.ts +6 -1
- package/dist/sessions/todo.js +11 -4
- package/dist/sessions/todo.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +12 -1
- package/dist/sessions/tool-display.js +50 -1
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +1 -1
- package/dist/sessions/turn-context.js +11 -7
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +67 -11
- package/dist/sessions/turn.js +413 -111
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/wakeup-pause.d.ts +59 -0
- package/dist/sessions/wakeup-pause.js +181 -0
- package/dist/sessions/wakeup-pause.js.map +1 -0
- package/dist/settings.d.ts +20 -2
- package/dist/settings.js +45 -9
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/fake.d.ts +4 -4
- package/dist/sidecar/fake.js +5 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/spa.js +2 -1
- package/dist/spa.js.map +1 -1
- package/dist/startup.d.ts +13 -5
- package/dist/startup.js +48 -0
- package/dist/startup.js.map +1 -1
- package/dist/test/harness.js +2 -0
- package/dist/test/harness.js.map +1 -1
- package/dist/test/ts-worker-hooks.d.ts +1 -0
- package/dist/test/ts-worker-hooks.js +24 -0
- package/dist/test/ts-worker-hooks.js.map +1 -0
- package/dist/text/diff.d.ts +9 -14
- package/dist/text/diff.js +74 -108
- package/dist/text/diff.js.map +1 -1
- package/dist/tools/crons.js +45 -8
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +53 -0
- package/dist/tools/dashboard-plugins.js +188 -0
- package/dist/tools/dashboard-plugins.js.map +1 -0
- package/dist/tools/extensions.d.ts +13 -1
- package/dist/tools/extensions.js +132 -60
- package/dist/tools/extensions.js.map +1 -1
- package/dist/tools/health.d.ts +32 -0
- package/dist/tools/health.js +111 -19
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/insights.d.ts +8 -2
- package/dist/tools/insights.js +18 -2
- package/dist/tools/insights.js.map +1 -1
- package/dist/tools/kanban.d.ts +41 -1
- package/dist/tools/kanban.js +113 -2
- package/dist/tools/kanban.js.map +1 -1
- package/dist/tools/mcp.d.ts +15 -1
- package/dist/tools/mcp.js +99 -5
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/project-os.d.ts +11 -0
- package/dist/tools/project-os.js +267 -0
- package/dist/tools/project-os.js.map +1 -0
- package/dist/tools/skills.d.ts +5 -1
- package/dist/tools/skills.js +5 -3
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/terminal.d.ts +1 -0
- package/dist/tools/terminal.js +1 -1
- package/dist/tools/terminal.js.map +1 -1
- package/dist/tools/updates.d.ts +33 -8
- package/dist/tools/updates.js +236 -57
- package/dist/tools/updates.js.map +1 -1
- package/dist/tools/wiki.d.ts +68 -0
- package/dist/tools/wiki.js +413 -0
- package/dist/tools/wiki.js.map +1 -0
- package/dist/workspace/escape.d.ts +61 -0
- package/dist/workspace/escape.js +149 -0
- package/dist/workspace/escape.js.map +1 -0
- package/dist/workspace/extract.d.ts +22 -0
- package/dist/workspace/extract.js +308 -0
- package/dist/workspace/extract.js.map +1 -0
- package/dist/workspace/fs.d.ts +19 -3
- package/dist/workspace/fs.js +96 -23
- package/dist/workspace/fs.js.map +1 -1
- package/dist/workspace/git.d.ts +18 -0
- package/dist/workspace/git.js +38 -25
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media.d.ts +4 -4
- package/dist/workspace/media.js +18 -45
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/preview.d.ts +28 -0
- package/dist/workspace/preview.js +110 -0
- package/dist/workspace/preview.js.map +1 -0
- package/dist/workspace/rollback.d.ts +36 -21
- package/dist/workspace/rollback.js +179 -270
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/unzip.js +4 -1
- package/dist/workspace/unzip.js.map +1 -1
- package/dist/workspace/upload.d.ts +9 -4
- package/dist/workspace/upload.js +9 -3
- package/dist/workspace/upload.js.map +1 -1
- package/dist/workspace/worktrees.d.ts +2 -1
- package/dist/workspace/worktrees.js +14 -15
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +4 -2
- package/sidecar/agent_dependency.json +4 -4
- package/sidecar/scripts/replay_sidecar.py +25 -1
- package/sidecar/talaria_sidecar/__init__.py +1 -1
- package/sidecar/talaria_sidecar/__main__.py +7 -1
- package/sidecar/talaria_sidecar/home.py +52 -2
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +39 -5
- package/sidecar/talaria_sidecar/methods/chat.py +431 -43
- package/sidecar/talaria_sidecar/methods/checkpoints.py +89 -0
- package/sidecar/talaria_sidecar/methods/commands.py +96 -2
- package/sidecar/talaria_sidecar/methods/cron.py +39 -8
- package/sidecar/talaria_sidecar/methods/gateway.py +32 -15
- package/sidecar/talaria_sidecar/methods/kanban.py +140 -19
- package/sidecar/talaria_sidecar/methods/mcp.py +1 -1
- package/sidecar/talaria_sidecar/methods/process.py +115 -2
- package/sidecar/talaria_sidecar/methods/profiles.py +84 -8
- package/sidecar/talaria_sidecar/methods/providers.py +27 -8
- package/sidecar/talaria_sidecar/methods/runtime.py +3 -0
- package/sidecar/talaria_sidecar/methods/state_db.py +39 -5
- package/sidecar/talaria_sidecar/methods/text.py +17 -1
- package/sidecar/talaria_sidecar/methods/usage.py +257 -4
- package/sidecar/talaria_sidecar/runtime.py +17 -1
- package/static/dist/FILES.txt +127 -125
- package/static/dist/assets/AppShell-g3bkgnvt.js +1 -0
- package/static/dist/assets/ChatPage-oo7bdan6.js +25 -0
- package/static/dist/assets/Dialog-g355qxhp.js +1 -0
- package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +1 -0
- package/static/dist/assets/HubRoute-lmkrbw47.js +2 -0
- package/static/dist/assets/{Markdown-jqrx5es0.js → Markdown-j4kut4j2.js} +125 -125
- package/static/dist/assets/RightPanel-ohlsdzfl.js +1 -0
- package/static/dist/assets/Select-kddq75qc.js +1 -0
- package/static/dist/assets/SessionListPanel-lu3oy0hw.js +1 -0
- package/static/dist/assets/SettingsLayout-o9bv4mgb.js +1 -0
- package/static/dist/assets/States-eq6fe29m.js +1 -0
- package/static/dist/assets/Toaster-pc247u9f.js +1 -0
- package/static/dist/assets/_app-f8n1i0m1.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-gfbzpoou.js +1 -0
- package/static/dist/assets/_app.index-bzzv80hk.js +1 -0
- package/static/dist/assets/_app.insights-dzixbryi.js +1 -0
- package/static/dist/assets/_app.kanban-c6hep2us.js +1 -0
- package/static/dist/assets/_app.logs-i69qds98.js +1 -0
- package/static/dist/assets/_app.memory-kl4vf0gd.js +1 -0
- package/static/dist/assets/_app.profiles-b8zinmyp.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-hh27wwu2.js → _app.session._sessionId-fn1bhde6.js} +1 -1
- package/static/dist/assets/_app.settings-cg8plwmf.js +1 -0
- package/static/dist/assets/_app.settings._section-bkwbi9zn.js +3 -0
- package/static/dist/assets/_app.skills-cs47sbvs.js +1 -0
- package/static/dist/assets/_app.tasks-dwvk64tc.js +1 -0
- package/static/dist/assets/_app.todos-c8xjm8p3.js +1 -0
- package/static/dist/assets/_app.workspaces-ehvd1gds.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-oity9y4o.js → abnfDiagram-VCTEODGH-c8kdixke.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-ceaepnyr.js → architectureDiagram-5GKGNRK7-cku06x1p.js} +1 -1
- package/static/dist/assets/{back-eu64w89w.js → back-h7aun7ew.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-n79st4pj.js → blockDiagram-I7D4REHJ-jzthp1wa.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-mi73akzk.js → c4Diagram-7LVT6UL2-mwr67mt8.js} +1 -1
- package/static/dist/assets/{cancel-mzvkdjg8.js → cancel-fhi0vcnu.js} +1 -1
- package/static/dist/assets/channel-b2zo73z1.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-laat0g1q.js → chunk-4HAMMTFA-gpq1y228.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-0psy1tr2.js → chunk-75Z2AOVW-ktje58c4.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-lw8uox8x.js → chunk-DU6HZSFF-ofajdixh.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-erassib3.js → chunk-F27PBJKO-cmev85ti.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-fz7rx619.js → chunk-GMAD6QVW-htjgt22y.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-gdb0epb2.js → chunk-GVQU2GXP-qndvoxl7.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-ea8t26qw.js → chunk-IMKFNOWR-e5bkd5q6.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-mb1gu9kz.js → chunk-L3NEJ4N5-mj1155r6.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-eovmccvy.js → chunk-OSK3NFVY-e7gkikcw.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-bo55zjo5.js → chunk-P2QGCYS3-f7t54xfm.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-ja7ohe4h.js → chunk-POPQ4Y6H-i927v9oa.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-e30g9nus.js → chunk-PWAF6VOD-m84lmnyt.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-gbb798f7.js → chunk-SHT3W25Y-mi0tlcu9.js} +1 -1
- package/static/dist/assets/{chunk-SVP7TREG-h2iw8urm.js → chunk-SVP7TREG-dnau702j.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-fd2cottz.js → chunk-TICWLB2K-edem1gkg.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +1 -0
- package/static/dist/assets/{copied-h65fc3cx.js → copied-hcplv46p.js} +1 -1
- package/static/dist/assets/copy-magtofyp.js +1 -0
- package/static/dist/assets/{cynefin-OW5HDTMX-lbltyl5j.js → cynefin-OW5HDTMX-siqlulkl.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-lxhsqq4a.js → cynefinDiagram-5FMLGOSQ-i648tah9.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-gn4nnaln.js → dagre-GXQ25YYZ-mwn6uc17.js} +1 -1
- package/static/dist/assets/{diagram-S7CK7UJ4-pa3ij6q7.js → diagram-S7CK7UJ4-l8abxaes.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-zbipyz57.js → diagram-UQ7AKVKN-mekq7zgq.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-d1kbpzgf.js → diagram-VSXAHHWV-fnky6lvc.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-i3ty14xl.js → diagram-VX7I27RA-xe6r5qfw.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-m8xefqa7.js → diagram-Z3DM3KII-nq07dg7k.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-nz7wj05l.js → ebnfDiagram-PWID7BFC-c62ipm3g.js} +1 -1
- package/static/dist/assets/endpoints-klbxod4t.js +64 -0
- package/static/dist/assets/{erDiagram-RLTQ6QDP-fa41ib83.js → erDiagram-RLTQ6QDP-h39kmnzy.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-n7zq01qu.js → ganttDiagram-EL5Y4UJY-ngbsyp37.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-c89emb07.js → gitGraph-4MIJSDKK-fr2i7caa.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-cz7cgr6w.js → gitGraphDiagram-WWUBYQGX-eravdti4.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-d9bapmni.js → highlighted-body-KPVGNVTW-jwpop9p8.js} +1 -1
- package/static/dist/assets/host-i828xuo7.js +1 -0
- package/static/dist/assets/index-eh8zilnv.css +2 -0
- package/static/dist/assets/index-r648f4o1.js +11 -0
- package/static/dist/assets/{info-A6RAGUB7-ndddhhhh.js → info-A6RAGUB7-h11x0dxo.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-ccxigcpf.js → infoDiagram-27XIBGKW-dfh47dw9.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-le0z9vtc.js → ishikawaDiagram-5VMMS53U-i6ypq351.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-ivsmb26o.js → journeyDiagram-3NMN7TZE-lduf1z95.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-h6n2awt0.js → kanban-definition-UXKFOSKX-e18bu4pg.js} +1 -1
- package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +1 -0
- package/static/dist/assets/{line-dmzlfe0u.js → line-meizqsvn.js} +1 -1
- package/static/dist/assets/link-oot2yn73.js +1 -0
- package/static/dist/assets/login-gu28l9ou.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-gwyd5bnq.js → mermaid-parser.core-ntne45i9.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-l68gg5h7.js → mindmap-definition-YA3MSWOX-27b26eok.js} +1 -1
- package/static/dist/assets/onboarding-lkphzu1s.js +1 -0
- package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-gpv06065.js → packet-AYTQ26CC-l6jdka8p.js} +1 -1
- package/static/dist/assets/{pegDiagram-XKGWAZYB-e52vs23u.js → pegDiagram-XKGWAZYB-g1er3bxj.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-l6m7nz5s.js → pie-WAS4IAKB-i0ylcrt9.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-b4is36bq.js → pieDiagram-E7YTZNPT-fbz1e55z.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-m8vt2788.js → quadrantDiagram-AXDQQJYC-hhue7d8o.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-nxbrhiov.js → radar-RG4KPBEZ-f9b70aa4.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-222mu9hi.js → railroad-74A4TZTK-lgsuoeda.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-o8lldfzv.js → railroadDiagram-O6MQD6OU-eqpy9fnz.js} +1 -1
- package/static/dist/assets/refresh-abtu93ke.js +1 -0
- package/static/dist/assets/refresh-cw-hlwb02qv.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-ovyq38kv.js → requirementDiagram-BXWQKSXE-ogf6puhi.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-ou7lznor.js → sankeyDiagram-P5KCCOFB-fqb8xcjf.js} +1 -1
- package/static/dist/assets/{saved-bazmcup9.js → saved-girnabkf.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-ovckdzf2.js → sequenceDiagram-WJ2MYXX4-dlcz6qjz.js} +1 -1
- package/static/dist/assets/{settings_section_extensions_title-ertwwhlx.js → settings_section_extensions_title-h14cwk6f.js} +1 -1
- package/static/dist/assets/share._token-kl0r7ido.js +2 -0
- package/static/dist/assets/sliders-horizontal-n1zm34qq.js +1 -0
- package/static/dist/assets/sse-iwku0sjo.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-fcrexkkv.js → stateDiagram-D77RDMKH-fyoznpcs.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-f7usu8fw.js → swimlanes-42K2YHIH-oefldhfy.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-gp1hvz9f.js → timeline-definition-24CTP7MA-b1vl2zil.js} +1 -1
- package/static/dist/assets/todoStore-jjr5yr78.js +1 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-gb9gf11u.js → treeView-Q6P3EWNA-f7hcci5o.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-nuvfnvvi.js → treemap-WGGIJYW6-nl85071a.js} +1 -1
- package/static/dist/assets/units-kgxzxfhi.js +1 -0
- package/static/dist/assets/{untitled-htelhc68.js → untitled-cikfjntn.js} +1 -1
- package/static/dist/assets/{useForm-h2t0u14e.js → useForm-kqoqg0k9.js} +1 -1
- package/static/dist/assets/useLocale-o59tbiea.js +1 -0
- package/static/dist/assets/{useParams-dqd64jv7.js → useParams-b4dq6n4q.js} +1 -1
- package/static/dist/assets/{useSearch-j30pvsy7.js → useSearch-ipfmv3he.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-fzchbzaj.js → vennDiagram-4TSXK5OY-h5hzbk0n.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-m47mhfbh.js → wardley-WFR3VGLG-jftvpm3e.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-odg6ct1z.js → wardleyDiagram-VM6X3IG4-hedh85ge.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-mrhms5um.js → xychartDiagram-S5SC5T6Z-e49523ab.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +2 -2
- package/static/dist/assets/AppShell-ef3bhu32.js +0 -1
- package/static/dist/assets/Button-nx37f4u1.js +0 -1
- package/static/dist/assets/ChatPage-cdcpykcg.js +0 -25
- package/static/dist/assets/Dialog-mec42hvb.js +0 -1
- package/static/dist/assets/HubRoute-ecb65zpr.js +0 -2
- package/static/dist/assets/RightPanel-d1lgvk4s.js +0 -1
- package/static/dist/assets/Select-h4ivl9qf.js +0 -1
- package/static/dist/assets/SessionListPanel-ewib046b.js +0 -1
- package/static/dist/assets/SettingsLayout-lx654cup.js +0 -1
- package/static/dist/assets/States-ehsb6lzo.js +0 -1
- package/static/dist/assets/Toaster-mch1pa3t.js +0 -1
- package/static/dist/assets/_app-m8d961ef.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-mp33hbzi.js +0 -1
- package/static/dist/assets/_app.index-dk6zp7e0.js +0 -1
- package/static/dist/assets/_app.insights-jt2fk4i1.js +0 -1
- package/static/dist/assets/_app.kanban-if4pubeu.js +0 -1
- package/static/dist/assets/_app.logs-mfaw9m6g.js +0 -1
- package/static/dist/assets/_app.memory-h3eaarb6.js +0 -1
- package/static/dist/assets/_app.profiles-ienktovs.js +0 -1
- package/static/dist/assets/_app.settings-dpoe0bzf.js +0 -1
- package/static/dist/assets/_app.settings._section-l468ynag.js +0 -3
- package/static/dist/assets/_app.skills-kcbncc8i.js +0 -1
- package/static/dist/assets/_app.tasks-occdrqhw.js +0 -2
- package/static/dist/assets/_app.todos-nnke2rcc.js +0 -1
- package/static/dist/assets/_app.workspaces-cdlatvwj.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-hj11333z.js +0 -1
- package/static/dist/assets/channel-mbvmf2vv.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-o8r05k0n.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-o8r05k0n.js +0 -1
- package/static/dist/assets/copy-e6977uwq.js +0 -1
- package/static/dist/assets/endpoints-k7yf25m0.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-kpbq09bx.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-lcz0kx14.js +0 -1
- package/static/dist/assets/index-h00usru2.js +0 -11
- package/static/dist/assets/index-n73ijkv2.css +0 -2
- package/static/dist/assets/lazyRouteComponent-m1rtyk4t.js +0 -1
- package/static/dist/assets/link-budeg5t9.js +0 -1
- package/static/dist/assets/login-kjhasvp8.js +0 -1
- package/static/dist/assets/matchContext-f8oy3lo7.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-jb5d211j.js +0 -1
- package/static/dist/assets/onboarding-cdfrk0pj.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-kdvgl6x8.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-itiqslby.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-go7kov78.js +0 -1
- package/static/dist/assets/refresh-cw-kbcje1uy.js +0 -1
- package/static/dist/assets/search-jen74z4u.js +0 -1
- package/static/dist/assets/share._token-dan9hgvj.js +0 -2
- package/static/dist/assets/sliders-horizontal-f9cb3lh3.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-lcfraua2.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-ogmanni0.js +0 -8
- package/static/dist/assets/todoStore-eqtid4r4.js +0 -1
- package/static/dist/assets/useLocale-hcsb84pj.js +0 -1
- package/static/dist/assets/useNavigate-nwy70ztj.js +0 -1
package/dist/sessions/service.js
CHANGED
|
@@ -3,16 +3,20 @@ 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';
|
|
6
|
+
import { allSessions, buildSessionListPayload, CLI_IDENTITY_FIELDS, isClaimableCliSource, isMessagingSessionRecord, ownerLockCandidates, withOwnerLocks, withSessionWireFlags, lineageRootId, mergeCliSidebarMetadata, sessionListResponse, sessionSearchMatches, sessionSearchMessageText, sessionSearchPreview, sessionSearchTerms } from './list.js';
|
|
7
7
|
import { anchorSceneIntOrNull, fullToolResult, hydrateAnchorActivityScenes, normalizeAnchorSceneMessageRef, readAnchorSceneRows, storeAnchorScene, withTurnIds } from './anchor.js';
|
|
8
|
-
import { isSafeSessionId, lastMessageTimestamp, Session, stripAttachedFilesMarker, titleFrom } from './session.js';
|
|
8
|
+
import { COMPRESSION_RECOVERY_ACTION_START_FOCUSED, compressionRecoveryPayload, 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 { auditSessionRecovery, repairSafeSessionRecovery } from './recovery.js';
|
|
13
|
+
import { agentSessionRowsExisting, agentSessionRowsExistingAsync, CONVERSATION_ROUND_THRESHOLD, countConversationRounds, stateDbCompressionLineage, stateDbTailToolContent, stateDbLineageReport, stateDbSessionMessages, stateDbSessionRead, stateDbSessionRow, stateDbSessionSources, stateDbSessionSourcesAsync, stateDbTimestampSeconds } from './state-db.js';
|
|
14
|
+
import { completionIncomplete, fallbackHandoffSummary, HANDOFF_SYSTEM_PROMPT, handoffMarker, handoffPayload, handoffTranscript, messageHandoffPayload, sameHandoff } from './handoff.js';
|
|
15
|
+
import { anchorMessageKey, anchorSummary, CompressionJobs, compressionReference, visibleMessagesForAnchor } from './compress.js';
|
|
13
16
|
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';
|
|
17
|
+
import { agentSteerText, attachedFilesPrompt, attachmentObjects, checkpointTurnStart, dedupeContext, isContextCompressionMarker, journalOutputRows, looksLikeCurrentUserTurn, stoppedTurnContext, mergeSessionMessagesAppendOnly, messageIdentity, pendingUserRow, reasoningFieldsText, sanitizeMessagesForApi, stateDbSeenId, stripWorkspacePrefix, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withSceneRowMedia, withPendingUserTurn, withToolCallOutcomes, withoutRunningTurnOutput } from './merge.js';
|
|
15
18
|
import { withBackgroundUpdates } from './background-updates.js';
|
|
19
|
+
import { sourceKind } from './source-kind.js';
|
|
16
20
|
import { withBackgroundLinks } from './background-tasks.js';
|
|
17
21
|
import { messagesForLimitedPayload, messageWindowForDisplay, MAX_MSG_LIMIT, parseMsgLimit, toolCallsForMessageWindow } from './window.js';
|
|
18
22
|
import { redactText } from '../redact.js';
|
|
@@ -50,6 +54,9 @@ export async function ensureAgentRuntimeCurrent(sidecar) {
|
|
|
50
54
|
function staleRuntimeFailure(error) {
|
|
51
55
|
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
56
|
}
|
|
57
|
+
/** TAL-536: the most of a dead run's journal stale-stream cleanup reads (Python's recovery window). */
|
|
58
|
+
const RECOVERY_JOURNAL_MAX_BYTES = 8 * 1024 * 1024;
|
|
59
|
+
const RECOVERY_JOURNAL_MAX_ROWS = 65536;
|
|
53
60
|
const isDict = (v) => Boolean(v) && typeof v === 'object' && !Array.isArray(v);
|
|
54
61
|
export class SessionService {
|
|
55
62
|
deps;
|
|
@@ -60,6 +67,40 @@ export class SessionService {
|
|
|
60
67
|
this.compressionJobs = new CompressionJobs();
|
|
61
68
|
}
|
|
62
69
|
get store() { return this.deps.store; }
|
|
70
|
+
/**
|
|
71
|
+
* TAL-577: user-turn admission shared by `chat.start` and server-side wakeups, after `ensureAgentRuntimeCurrent`.
|
|
72
|
+
* A requested workspace must be trusted; the session's own one, when its directory is gone, falls back to the last one.
|
|
73
|
+
*/
|
|
74
|
+
turnWorkspace(s, requested) {
|
|
75
|
+
const ws = this.deps.workspaces;
|
|
76
|
+
if (requested !== undefined && requested !== null && requested !== '') {
|
|
77
|
+
try {
|
|
78
|
+
return ws.resolveTrusted(str(requested), s.profile);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new HttpFailure(400, error.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const [workspace, recovered] = ws.resolveImplicitWithRecovery(s.workspace, (p) => ws.lastWorkspace(p), s.profile);
|
|
86
|
+
if (recovered)
|
|
87
|
+
s.workspace = workspace;
|
|
88
|
+
return workspace;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new HttpFailure(400, error.message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** A `@provider:` id is split; any other model an explicit pick did not just choose is checked against the catalog (TAL-542). */
|
|
95
|
+
turnModel(s, body = {}) {
|
|
96
|
+
const requestedModel = str(body.model) || s.model;
|
|
97
|
+
const requestedProvider = 'model_provider' in body ? body.model_provider : s.model_provider;
|
|
98
|
+
const [model, provider] = this.deps.modelStateFromRequest(requestedModel, requestedProvider, s.model_provider);
|
|
99
|
+
if (model !== requestedModel || !model || body.explicit_model_pick === true)
|
|
100
|
+
return [model, provider, model !== requestedModel];
|
|
101
|
+
const repaired = this.deps.repairSessionModel?.(s.profile ?? null, model, provider);
|
|
102
|
+
return repaired ? [repaired[0], repaired[1], true] : [model, provider, false];
|
|
103
|
+
}
|
|
63
104
|
/**
|
|
64
105
|
* TAL-186: rewrites a session's media references to their URLs. A local path is rewritten only when `/api/media`
|
|
65
106
|
* serves it for this session, under the same resolution, so every rewritten reference loads.
|
|
@@ -146,6 +187,52 @@ export class SessionService {
|
|
|
146
187
|
}
|
|
147
188
|
/** TAL-358: batched `sessions.source` owners from the active profile's state.db. */
|
|
148
189
|
stateDbSources = (ids) => stateDbSessionSources(join(this.deps.profileHome(this.deps.activeProfile()), 'state.db'), ids);
|
|
190
|
+
/**
|
|
191
|
+
* TAL-590 (Python #3238): the read-only CLI or API-server imports among `rows` whose active-profile state.db row is
|
|
192
|
+
* gone, so their source was deleted outside Web and no Web action could remove them. An unreadable state.db proves
|
|
193
|
+
* nothing (`agentSessionRowsExisting`); another profile's session or one with a run the runtime still holds is never
|
|
194
|
+
* an orphan, while a stream id a crash left behind does not count as one.
|
|
195
|
+
*/
|
|
196
|
+
orphanedImports(rows) {
|
|
197
|
+
const candidates = this.orphanCandidates(rows);
|
|
198
|
+
if (!candidates.length)
|
|
199
|
+
return new Set();
|
|
200
|
+
const existing = agentSessionRowsExisting(this.stateDbPath(null), candidates);
|
|
201
|
+
return new Set(candidates.filter((sid) => !existing.has(sid)));
|
|
202
|
+
}
|
|
203
|
+
orphanCandidates(rows) {
|
|
204
|
+
return rows.filter((r) => r.read_only && ['cli', 'api'].includes(sourceKind(r)) && this.visibleToActiveProfile(str(r.profile) || null) && !this.activeRunBlocking(str(r.session_id)))
|
|
205
|
+
.map((r) => str(r.session_id));
|
|
206
|
+
}
|
|
207
|
+
isOrphanedImport(sid) {
|
|
208
|
+
try {
|
|
209
|
+
return this.orphanedImports([this.store.get(sid, { metadataOnly: true }).compact()]).size > 0;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The list's owner locks and orphan prune (an orphan's sidecar goes, as nothing else ever removes it), with their
|
|
217
|
+
* state.db reads made on the listing worker up front so the synchronous build never opens state.db (TAL-920).
|
|
218
|
+
*/
|
|
219
|
+
async listStateDbLookups(rows) {
|
|
220
|
+
const dbPath = this.stateDbPath(null);
|
|
221
|
+
const ownerIds = ownerLockCandidates(rows);
|
|
222
|
+
const owners = ownerIds.length ? await stateDbSessionSourcesAsync(dbPath, ownerIds) : new Map();
|
|
223
|
+
const checked = new Set(this.orphanCandidates(rows));
|
|
224
|
+
const existing = checked.size ? await agentSessionRowsExistingAsync(dbPath, checked) : new Set();
|
|
225
|
+
return {
|
|
226
|
+
stateDbSources: () => owners,
|
|
227
|
+
pruneOrphanedImports: (current) => {
|
|
228
|
+
// Only an id the worker checked can be an orphan; a row that became a candidate meanwhile waits for the next list.
|
|
229
|
+
const orphans = new Set(this.orphanCandidates(current).filter((sid) => checked.has(sid) && !existing.has(sid)));
|
|
230
|
+
for (const sid of orphans)
|
|
231
|
+
this.store.deleteFiles(sid);
|
|
232
|
+
return orphans;
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
149
236
|
/** The persisted flag folded with the state.db owner lock (TAL-358): the one read-only rule for mutation gates and the wire. */
|
|
150
237
|
isReadOnly(s) {
|
|
151
238
|
if (s.read_only)
|
|
@@ -174,6 +261,7 @@ export class SessionService {
|
|
|
174
261
|
const synth = this.claimOrSynthesizeCliSession(sid);
|
|
175
262
|
if (synth.session && synth.reason === 'materialized') {
|
|
176
263
|
this.store.save(synth.session);
|
|
264
|
+
this.deps.titleClaimed?.(synth.session);
|
|
177
265
|
return synth.session;
|
|
178
266
|
}
|
|
179
267
|
if (synth.session)
|
|
@@ -185,9 +273,18 @@ export class SessionService {
|
|
|
185
273
|
}
|
|
186
274
|
/** The Agent's state.db rows for this session (empty for subagent views, which never merge). */
|
|
187
275
|
stateDbRows(s) {
|
|
276
|
+
return this.stateDbRead(s).rows;
|
|
277
|
+
}
|
|
278
|
+
/** `stateDbRows` and whether the read succeeded on a state.db whose messages carry ids (TAL-493). */
|
|
279
|
+
stateDbRead(s) {
|
|
188
280
|
if (str(s.source_tag || s.raw_source || s.session_source).trim().toLowerCase() === 'subagent')
|
|
189
|
-
return [];
|
|
190
|
-
|
|
281
|
+
return { rows: [], idCapable: false, ok: false };
|
|
282
|
+
// TAL-529: after a compression rotation the Agent writes to the continuation, so the read follows the recorded lineage.
|
|
283
|
+
const lineage = s.state_db_lineage?.[0] === s.session_id ? s.state_db_lineage : null;
|
|
284
|
+
return stateDbSessionRead(this.stateDbPath(s.profile), s.session_id, { stitch: false, lineage });
|
|
285
|
+
}
|
|
286
|
+
stateDbPath(profile) {
|
|
287
|
+
return join(this.deps.profileHome(profile ?? this.deps.activeProfile()), 'state.db');
|
|
191
288
|
}
|
|
192
289
|
/**
|
|
193
290
|
* Python `_merged_session_messages_for_display`: the sidecar transcript merged append-only with the Agent's state.db
|
|
@@ -197,7 +294,115 @@ export class SessionService {
|
|
|
197
294
|
mergedTranscript(s, local = s.messages, stateRows = this.stateDbRows(s)) {
|
|
198
295
|
if (!stateRows.length)
|
|
199
296
|
return local;
|
|
200
|
-
return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed });
|
|
297
|
+
return mergeSessionMessagesAppendOnly(local, stateRows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed, stateDbSeenId: currentStateDbSeenId(s, stateRows) });
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* TAL-493: record the highest state.db id this boundary or settled turn read, under the session lock in the same save,
|
|
301
|
+
* so the merge never replays a covered row and appends every row committed after the read. Call it after the boundary
|
|
302
|
+
* fields are set: the marker holds only while they and the row it names stay as recorded.
|
|
303
|
+
*/
|
|
304
|
+
markStateDbSeen(s, read = this.stateDbRead(s), opts = {}) {
|
|
305
|
+
// A failed read says nothing. A settlement keeps the marker; a boundary drops it (fail closed): it may cut rows the
|
|
306
|
+
// marker still admits without moving a field, and the timestamp rules honour its watermark.
|
|
307
|
+
if (!read.ok) {
|
|
308
|
+
if (opts.boundary) {
|
|
309
|
+
s.state_db_seen_id = null;
|
|
310
|
+
s.state_db_seen_boundary = null;
|
|
311
|
+
s.state_db_seen_stamp = null;
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
// A successful read with no rows yet is a baseline: every row the session gets later is new.
|
|
316
|
+
s.state_db_seen_id = stateDbSeenId(read.rows) ?? (read.idCapable ? 0 : null);
|
|
317
|
+
s.state_db_seen_boundary = stateDbMarkKey(s, read.rows, s.state_db_seen_id);
|
|
318
|
+
s.state_db_seen_stamp = s.updated_at;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* TAL-493: a turn's settlement. Each state.db row the merge shows now is matched to what the turn holds: a row already
|
|
322
|
+
* in the context it started from (by id), or one row the Agent added this turn (`agentRows` past that context, matched
|
|
323
|
+
* one to one by full text). An unmatched row is a CLI or gateway row committed while the turn ran: it joins the
|
|
324
|
+
* transcript and the model context before the marker passes it. Without the Agent's rows (`agentRows` empty), rows past
|
|
325
|
+
* `startId` (the highest id the turn read when it started) are its own unreported work: covered, never shown. Rows the
|
|
326
|
+
* turn started from but the transcript lacks (a CLI continuation read into its context) are kept before the turn.
|
|
327
|
+
*/
|
|
328
|
+
settleStateDb(s, turn) {
|
|
329
|
+
const read = turn.read ?? this.stateDbRead(s);
|
|
330
|
+
const startedWith = new Set(turn.previousContext.flatMap((m) => (typeof m._state_db_row_id === 'number' ? [m._state_db_row_id] : [])));
|
|
331
|
+
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);
|
|
332
|
+
const added = new Map();
|
|
333
|
+
for (const m of withoutRows(turn.agentRows ?? [], turn.previousContext))
|
|
334
|
+
added.set(settledIdentity(m), (added.get(settledIdentity(m)) ?? 0) + 1);
|
|
335
|
+
const ownWorkAfter = turn.agentRows?.length ? null : turn.startId ?? null;
|
|
336
|
+
// Without a current marker, the turn's own starting read is the baseline: rows committed after it are judged here.
|
|
337
|
+
const seenId = currentStateDbSeenId(s, read.rows) ?? turn.startId ?? null;
|
|
338
|
+
const shown = read.rows.length ? mergeSessionMessagesAppendOnly(s.messages, read.rows, { truncationWatermark: s.truncation_watermark, compressedWatermark: s.truncation_watermark_compressed, stateDbSeenId: seenId }) : s.messages;
|
|
339
|
+
const fresh = new Set(shown.slice(s.messages.length));
|
|
340
|
+
// The turn's own rows the merge already matched to their local copies use up their share before any shown row can.
|
|
341
|
+
if (seenId !== null) {
|
|
342
|
+
for (const m of read.rows) {
|
|
343
|
+
const id = m._state_db_row_id;
|
|
344
|
+
if (typeof id !== 'number' || id <= seenId || startedWith.has(id) || fresh.has(m))
|
|
345
|
+
continue;
|
|
346
|
+
const left = added.get(settledIdentity(m)) ?? 0;
|
|
347
|
+
if (left > 0)
|
|
348
|
+
added.set(settledIdentity(m), left - 1);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const missed = [...fresh].filter((m) => {
|
|
352
|
+
const id = m._state_db_row_id;
|
|
353
|
+
if (typeof id === 'number' && startedWith.has(id))
|
|
354
|
+
return false;
|
|
355
|
+
const key = settledIdentity(m);
|
|
356
|
+
const left = added.get(key) ?? 0;
|
|
357
|
+
if (left > 0) {
|
|
358
|
+
added.set(key, left - 1);
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
return !(ownWorkAfter !== null && typeof id === 'number' && id > ownWorkAfter);
|
|
362
|
+
});
|
|
363
|
+
if (earlier.length) {
|
|
364
|
+
const at = s.messages.findIndex((m) => m._turn_id === turn.turnId);
|
|
365
|
+
s.messages.splice(at < 0 ? s.messages.length : at, 0, ...copyJson(earlier));
|
|
366
|
+
}
|
|
367
|
+
if (missed.length) {
|
|
368
|
+
s.messages.push(...copyJson(missed));
|
|
369
|
+
if (s.context_messages.length)
|
|
370
|
+
s.context_messages.push(...copyJson(missed));
|
|
371
|
+
}
|
|
372
|
+
this.markStateDbSeen(s, read);
|
|
373
|
+
// TAL-529: a compression rotation moved the Agent to a continuation, whose first rows restate the compressed context.
|
|
374
|
+
// The read follows the new lineage from here, and everything it holds now is covered.
|
|
375
|
+
// ponytail: a CLI row committed to the continuation before this settlement is covered with them; telling them apart
|
|
376
|
+
// needs the Agent to mark the rows it writes at rotation.
|
|
377
|
+
const lineage = stateDbCompressionLineage(this.stateDbPath(s.profile), s.session_id);
|
|
378
|
+
if (JSON.stringify(lineage) !== JSON.stringify(s.state_db_lineage ?? [s.session_id])) {
|
|
379
|
+
s.state_db_lineage = lineage.length > 1 ? lineage : null;
|
|
380
|
+
this.markStateDbSeen(s);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* TAL-921: the model context a turn leaves when it ends without its settled result (an error, a Stop, a dead run), as a
|
|
385
|
+
* Stop's: the Agent's rows when they hold the turn's prompt, else its own rows among `stateRows` (the state.db rows
|
|
386
|
+
* committed after it started), then the prose that streamed past them. Null when it captured nothing the model can use.
|
|
387
|
+
*/
|
|
388
|
+
unsettledTurnContext(turn) {
|
|
389
|
+
const reported = turn.agentRows && checkpointTurnStart(turn.agentRows, turn.msgText, turn.historyLength) !== null ? turn.agentRows : null;
|
|
390
|
+
const own = reported ? null : runStateDbRows(turn.stateRows, turn.msgText);
|
|
391
|
+
const checkpoint = reported ?? (own && [...turn.previousContext, ...own]);
|
|
392
|
+
const context = stoppedTurnContext(turn.previousContext, checkpoint, turn.prompt, turn.msgText, turn.streamedText, turn.historyLength);
|
|
393
|
+
if (!context)
|
|
394
|
+
return null;
|
|
395
|
+
if (!own || !checkpoint || checkpointTurnStart(checkpoint, turn.msgText, turn.historyLength) === null)
|
|
396
|
+
return dedupeContext(context);
|
|
397
|
+
// The context holds the turn's own state.db rows from its prompt on. The Agent persisted the prompt's text only, so the
|
|
398
|
+
// prompt row carries what the turn sent (attached files, images). This turn's settlement covers the rows: without
|
|
399
|
+
// their ids, the next one never reads them as a CLI continuation the transcript lacks.
|
|
400
|
+
const at = turn.previousContext.length;
|
|
401
|
+
return dedupeContext(context.map((m, i) => {
|
|
402
|
+
if (i === at)
|
|
403
|
+
return { role: 'user', content: turn.prompt, ...(m.timestamp != null ? { timestamp: m.timestamp } : {}) };
|
|
404
|
+
return i > at && i < at + own.length ? Object.fromEntries(Object.entries(m).filter(([k]) => k !== '_state_db_row_id' && k !== '_state_db_generation')) : m;
|
|
405
|
+
}));
|
|
201
406
|
}
|
|
202
407
|
/**
|
|
203
408
|
* Python `reconciled_state_db_messages_for_session(prefer_context=True)`: the model history is the owner context
|
|
@@ -217,15 +422,30 @@ export class SessionService {
|
|
|
217
422
|
const pending = this.pendingTurn(s);
|
|
218
423
|
return pending ? withPendingUserTurn(context, pending) : context;
|
|
219
424
|
}
|
|
425
|
+
/** Python `_is_messaging_session_id`: from the WebUI metadata or the Agent's row. */
|
|
426
|
+
isMessagingSession(sid) {
|
|
427
|
+
try {
|
|
428
|
+
if (isMessagingSessionRecord(this.store.get(sid, { metadataOnly: true }).compact()))
|
|
429
|
+
return true;
|
|
430
|
+
}
|
|
431
|
+
catch { /* absent */ }
|
|
432
|
+
const meta = this.lookupCliMeta(sid);
|
|
433
|
+
return meta !== null && isMessagingSessionRecord(meta);
|
|
434
|
+
}
|
|
220
435
|
/** Python `_lookup_cli_session_metadata`: the sidebar row for a state.db session in the active profile. */
|
|
221
436
|
lookupCliMeta(sid) {
|
|
222
437
|
try {
|
|
223
|
-
|
|
438
|
+
if (isClaudeCodeSessionId(sid))
|
|
439
|
+
return this.claudeCodeRows().find((r) => str(r.session_id) === sid) ?? null;
|
|
440
|
+
return this.deps.cliSessionsSync(this.deps.activeProfile(), { sourceFilter: null }).rows.find((r) => str(r.session_id) === sid) ?? null;
|
|
224
441
|
}
|
|
225
442
|
catch {
|
|
226
443
|
return null;
|
|
227
444
|
}
|
|
228
445
|
}
|
|
446
|
+
claudeCodeRows() {
|
|
447
|
+
return this.deps.claudeCode?.rows(this.deps.workspaces.lastWorkspace(this.deps.activeProfile())) ?? [];
|
|
448
|
+
}
|
|
229
449
|
/** Python `_session_index_marks_was_webui`: an index row that once owned a WebUI sidecar (self-heal 404). */
|
|
230
450
|
indexMarksWasWebui(sid) {
|
|
231
451
|
let entries;
|
|
@@ -262,7 +482,7 @@ export class SessionService {
|
|
|
262
482
|
const subagentChild = stateDbSource === 'subagent';
|
|
263
483
|
if ((this.indexMarksWasWebui(sid) || (this.store.wasDeleted(sid) && ['', 'webui', 'fork'].includes(stateDbSource))) && !subagentChild)
|
|
264
484
|
return { session: null, reason: 'was_webui' };
|
|
265
|
-
const msgs = stateDbSessionMessages(dbPath, sid, { stitch: true });
|
|
485
|
+
const msgs = isClaudeCodeSessionId(sid) ? this.deps.claudeCode?.messages(sid) ?? [] : stateDbSessionMessages(dbPath, sid, { stitch: true });
|
|
266
486
|
if (!msgs.length)
|
|
267
487
|
return { session: null, reason: 'no_foreign_state' };
|
|
268
488
|
const meta = { ...(metaIn ?? this.lookupCliMeta(sid) ?? {}) };
|
|
@@ -309,7 +529,16 @@ export class SessionService {
|
|
|
309
529
|
throw error;
|
|
310
530
|
}
|
|
311
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* TAL-551: a Claude Code import is a live view of a transcript Claude Code keeps writing. Web never stores a copy,
|
|
534
|
+
* which would freeze its messages and pin it to one profile.
|
|
535
|
+
*/
|
|
536
|
+
rejectClaudeCode(sid, verb) {
|
|
537
|
+
if (isClaudeCodeSessionId(sid))
|
|
538
|
+
throw new HttpFailure(400, `Claude Code sessions are view-only and cannot be ${verb} from WebUI`);
|
|
539
|
+
}
|
|
312
540
|
rejectSubagent(sid, verb) {
|
|
541
|
+
this.rejectClaudeCode(sid, verb);
|
|
313
542
|
if (this.isSubagentViewOnly(sid))
|
|
314
543
|
throw new HttpFailure(400, `Subagent sessions are view-only and cannot be ${verb} from WebUI`);
|
|
315
544
|
}
|
|
@@ -321,8 +550,10 @@ export class SessionService {
|
|
|
321
550
|
publicSession(s, withMessages = true) {
|
|
322
551
|
const payload = this.wireRow(s);
|
|
323
552
|
// Mutation replies replace a client's transcript, so they carry the same server-built scenes as the detail.
|
|
324
|
-
if (withMessages)
|
|
553
|
+
if (withMessages) {
|
|
325
554
|
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) }));
|
|
555
|
+
payload.compression_reference = compressionReference(s, payload.messages);
|
|
556
|
+
}
|
|
326
557
|
return redactSessionData(payload, this.deps.redactEnabled());
|
|
327
558
|
}
|
|
328
559
|
/** `compact()` with the wire streaming/read-only flags (TAL-312), for replies that return the session row. */
|
|
@@ -385,7 +616,8 @@ export class SessionService {
|
|
|
385
616
|
}
|
|
386
617
|
this.clearStaleStreamState(s);
|
|
387
618
|
const journaled = loadMessages ? this.journaledActiveTurn(s) : null;
|
|
388
|
-
|
|
619
|
+
const stateRows = loadMessages ? this.stateDbRows(s) : [];
|
|
620
|
+
let transcript = loadMessages ? this.mergedTranscript(s, s.messages, stateRows) : [];
|
|
389
621
|
const pending = loadMessages ? this.pendingTurn(s) : null;
|
|
390
622
|
if (pending)
|
|
391
623
|
transcript = withPendingUserTurn(transcript, pending);
|
|
@@ -442,8 +674,10 @@ export class SessionService {
|
|
|
442
674
|
};
|
|
443
675
|
if (loadMessages)
|
|
444
676
|
raw.transcript_seq = journaled ? { stream_id: journaled.turnId, seq: 0 } : null;
|
|
445
|
-
if (loadMessages
|
|
446
|
-
attachTodoState(raw, all);
|
|
677
|
+
if (loadMessages)
|
|
678
|
+
attachTodoState(raw, all, s.extra[UNSETTLED_TODO_KEY]);
|
|
679
|
+
if (loadMessages)
|
|
680
|
+
raw.compression_reference = compressionReference(s, all);
|
|
447
681
|
if (mergedLast) {
|
|
448
682
|
raw.last_message_at = Math.max(Number(raw.last_message_at ?? 0) || 0, mergedLast);
|
|
449
683
|
raw.updated_at = Math.max(Number(raw.updated_at ?? 0) || 0, mergedLast);
|
|
@@ -457,10 +691,42 @@ export class SessionService {
|
|
|
457
691
|
raw.read_only = this.isReadOnly(s);
|
|
458
692
|
raw.assistant_name = this.assistantName(s);
|
|
459
693
|
raw.workspace_name = this.workspaceNames()(s);
|
|
694
|
+
raw.branched_from = this.branchedFrom(s);
|
|
460
695
|
withSessionWireFlags(raw, activeStreamIds);
|
|
696
|
+
// A metadata-only read lacks the history the gate counts, so it carries no flag.
|
|
697
|
+
if (loadMessages)
|
|
698
|
+
raw.can_compress = this.canCompress(s, raw.read_only === true, stateRows);
|
|
461
699
|
raw.pending_steers = raw.active_stream_id ? (this.deps.runtime.pendingSteers?.(str(raw.active_stream_id)) ?? []) : [];
|
|
462
700
|
return redactSessionData(raw, this.deps.redactEnabled());
|
|
463
701
|
}
|
|
702
|
+
/**
|
|
703
|
+
* TAL-454: the chat `/branch` copied, while it still loads (archived included). A compression continuation is also a
|
|
704
|
+
* fork of its parent but not a branch, so it links nowhere.
|
|
705
|
+
*/
|
|
706
|
+
branchedFrom(s) {
|
|
707
|
+
const parentId = s.parent_session_id;
|
|
708
|
+
if (!parentId || str(s.session_source).trim().toLowerCase() !== 'fork' || s.compression_recovery_source_session_id)
|
|
709
|
+
return null;
|
|
710
|
+
let title;
|
|
711
|
+
try {
|
|
712
|
+
const parent = this.store.get(parentId, { metadataOnly: true, promote: false, cacheOnMiss: false });
|
|
713
|
+
if (!this.visibleToActiveProfile(parent.profile))
|
|
714
|
+
return null;
|
|
715
|
+
title = parent.title;
|
|
716
|
+
}
|
|
717
|
+
catch {
|
|
718
|
+
// A read-only foreign parent (a cron run) has no sidecar and opens from the active profile's state.db while it
|
|
719
|
+
// has transcript rows and is not a deleted chat (`claimOrSynthesizeCliSession`'s `was_webui` and `no_foreign_state`).
|
|
720
|
+
const dbPath = this.stateDbPath(null);
|
|
721
|
+
const row = stateDbSessionRow(dbPath, parentId);
|
|
722
|
+
if (!row || this.store.wasDeleted(parentId) || this.indexMarksWasWebui(parentId))
|
|
723
|
+
return null;
|
|
724
|
+
if (!stateDbSessionMessages(dbPath, parentId, { stitch: true }).length)
|
|
725
|
+
return null;
|
|
726
|
+
title = row.title;
|
|
727
|
+
}
|
|
728
|
+
return { session_id: parentId, title: str(redactText(str(title) || 'Untitled', this.deps.redactEnabled())) };
|
|
729
|
+
}
|
|
464
730
|
/**
|
|
465
731
|
* TAL-316: the active run whose journal can replay its output from the start. The detail then leaves that output to the
|
|
466
732
|
* replay; with no active run, no identifiable prompt, or no complete journal, it returns the persisted transcript unchanged.
|
|
@@ -518,7 +784,7 @@ export class SessionService {
|
|
|
518
784
|
const merged = withSessionWireFlags(meta ? mergeCliSidebarMetadata(sess, meta) : sess, this.deps.runtime.activeStreamIds);
|
|
519
785
|
return redactSessionData(merged, this.deps.redactEnabled());
|
|
520
786
|
}
|
|
521
|
-
/** Clear persisted streaming flags when no live stream backs them (Python `_clear_stale_stream_state
|
|
787
|
+
/** Clear persisted streaming flags when no live stream backs them (Python `_clear_stale_stream_state`). */
|
|
522
788
|
clearStaleStreamState(session) {
|
|
523
789
|
const streamId = session.active_stream_id;
|
|
524
790
|
if (!streamId)
|
|
@@ -545,15 +811,41 @@ export class SessionService {
|
|
|
545
811
|
return false;
|
|
546
812
|
}
|
|
547
813
|
}
|
|
548
|
-
// Python `_materialize_pending_user_turn_before_error` (#1361)
|
|
549
|
-
//
|
|
814
|
+
// Python `_materialize_pending_user_turn_before_error` (#1361) and `_recover_dead_run_journal`: the prompt that was in
|
|
815
|
+
// flight becomes a durable user turn, the output its journal holds follows it, and an interruption marker closes a
|
|
816
|
+
// run that never finished with an answer, so a dead stream never silently drops what the user sent or what streamed.
|
|
817
|
+
const turnId = str(target.active_stream_id);
|
|
818
|
+
// ponytail: one bounded tail read (Python's recovery window); a journal past it recovers its latest output only.
|
|
819
|
+
const events = this.deps.journal?.readRunEventTail(target.session_id, turnId, RECOVERY_JOURNAL_MAX_BYTES, RECOVERY_JOURNAL_MAX_ROWS).events ?? [];
|
|
820
|
+
const { rows: output, answered } = journalOutputRows(events, turnId);
|
|
550
821
|
const pendingText = str(target.pending_user_message);
|
|
822
|
+
const attachments = [...target.pending_attachments];
|
|
823
|
+
// The run's starting read died with its worker: the state.db rows from before it started stand in for it. Its own
|
|
824
|
+
// rows open with its prompt and run to the next prompt; anything else another client wrote meanwhile stays.
|
|
825
|
+
const read = this.stateDbRead(target);
|
|
826
|
+
const runStart = target.pending_started_at || events[0]?.created_at || this.deps.now();
|
|
827
|
+
const before = read.rows.filter((m) => Number(m.timestamp) < runStart);
|
|
828
|
+
const runRows = read.rows.filter((m) => Number(m.timestamp) >= runStart);
|
|
829
|
+
const ownRows = target.hasPendingPrompt ? runStateDbRows(runRows, pendingText) : null;
|
|
830
|
+
// Without this turn's rows: an eager save already put its prompt in the transcript a fresh context falls back to.
|
|
831
|
+
const previousContext = this.modelContext(target, before).filter((m) => m._turn_id !== turnId);
|
|
551
832
|
if (target.hasPendingPrompt) {
|
|
552
|
-
const turnId = str(target.active_stream_id);
|
|
553
833
|
const startedAt = typeof target.pending_started_at === 'number' && target.pending_started_at > 0 ? target.pending_started_at : this.deps.now();
|
|
554
|
-
|
|
555
|
-
target.messages.
|
|
556
|
-
|
|
834
|
+
// An eager save already checkpointed this prompt as the turn's user row.
|
|
835
|
+
if (!target.messages.some((m) => m.role === 'user' && !m._steer && m._turn_id === turnId))
|
|
836
|
+
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 });
|
|
837
|
+
// The model context settles as a Stop's does: the Agent's own rows when it committed the prompt, else the prompt it
|
|
838
|
+
// was sent; then the prose that streamed past them.
|
|
839
|
+
const prompt = attachedFilesPrompt(pendingText, attachments);
|
|
840
|
+
const streamed = output.map((m) => str(m.content)).filter(Boolean).join('\n\n');
|
|
841
|
+
target.context_messages = this.unsettledTurnContext({ previousContext, prompt, msgText: pendingText, streamedText: streamed, historyLength: previousContext.length, stateRows: runRows }) ?? dedupeContext([...structuredClone(previousContext), { role: 'user', content: prompt }]);
|
|
842
|
+
}
|
|
843
|
+
if (target.hasPendingPrompt || output.length) {
|
|
844
|
+
target.messages.push(...output);
|
|
845
|
+
if (!answered)
|
|
846
|
+
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 });
|
|
847
|
+
// 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.
|
|
848
|
+
this.settleStateDb(target, { turnId, previousContext, agentRows: ownRows, startId: stateDbSeenId(before) ?? (read.idCapable ? 0 : null), read });
|
|
557
849
|
}
|
|
558
850
|
target.active_stream_id = null;
|
|
559
851
|
target.pending_user_message = null;
|
|
@@ -577,15 +869,18 @@ export class SessionService {
|
|
|
577
869
|
return true;
|
|
578
870
|
}
|
|
579
871
|
// ── list / search ────────────────────────────────────────────────────────
|
|
580
|
-
list(params) {
|
|
872
|
+
async list(params) {
|
|
581
873
|
const activeProfile = this.deps.activeProfile();
|
|
582
874
|
const wantState = params.showCliSessions || params.showCronSessions || params.showWebhookSessions || params.showKanbanSessions;
|
|
583
875
|
// Python reads every profile's state.db under all_profiles; this port projects the active profile only.
|
|
584
|
-
const cliRead = wantState ? this.deps.cliSessions(activeProfile, { sourceFilter: params.sourceFilter ?? null }) : undefined;
|
|
585
|
-
const
|
|
876
|
+
const cliRead = wantState ? await this.deps.cliSessions(activeProfile, { sourceFilter: params.sourceFilter ?? null }) : undefined;
|
|
877
|
+
const claudeCodeRows = params.showCliSessions && params.showClaudeCodeSessions ? this.claudeCodeRows() : [];
|
|
878
|
+
const cliRows = cliRead ? [...cliRead.rows, ...claudeCodeRows] : undefined;
|
|
586
879
|
const gatewayIdentity = loadGatewaySessionIdentityMap(join(this.deps.profileHome(activeProfile), 'sessions', 'sessions.json'));
|
|
587
880
|
const truncatedSources = cliRead?.truncated;
|
|
588
|
-
const
|
|
881
|
+
const webuiRows = allSessions(this.store, { sidebarMetadataOnly: true });
|
|
882
|
+
const lookups = await this.listStateDbLookups(webuiRows);
|
|
883
|
+
const payload = buildSessionListPayload(this.store, { ...params, ...(cliRows ? { cliRows } : {}), ...(truncatedSources ? { truncatedSources } : {}), gatewayIdentity, ...lookups, webuiRows, activeProfile, isolatedProfileMode: this.deps.isolatedProfileMode(), profilesMatch: this.deps.profilesMatch });
|
|
589
884
|
return sessionListResponse(payload, this.deps.runtime, this.deps.redactEnabled(), this.deps.now(), this.workspaceNames());
|
|
590
885
|
}
|
|
591
886
|
search(q, opts) {
|
|
@@ -609,15 +904,16 @@ export class SessionService {
|
|
|
609
904
|
const query = q.toLowerCase().trim();
|
|
610
905
|
if (!query)
|
|
611
906
|
return { sessions: sessions.map((s) => redactRow({ ...s })), all_profiles: opts.allProfiles, active_profile: activeProfile };
|
|
907
|
+
const terms = sessionSearchTerms(query);
|
|
612
908
|
const results = [];
|
|
613
909
|
for (const s of sessions) {
|
|
614
|
-
if (str(s.title)
|
|
910
|
+
if (sessionSearchMatches(str(s.title), terms)) {
|
|
615
911
|
results.push(redactRow({ ...s, match_type: 'title' }));
|
|
616
912
|
continue;
|
|
617
913
|
}
|
|
618
914
|
if (!opts.content)
|
|
619
915
|
continue;
|
|
620
|
-
const hit = this.contentMatch(str(s.session_id),
|
|
916
|
+
const hit = this.contentMatch(str(s.session_id), terms, opts.depth);
|
|
621
917
|
if (hit)
|
|
622
918
|
results.push(redactRow({ ...s, ...hit }));
|
|
623
919
|
}
|
|
@@ -628,8 +924,8 @@ export class SessionService {
|
|
|
628
924
|
* archived, projection and order), narrowed to `projectId` (`none`: rows without a project), then matched on the
|
|
629
925
|
* title, then the metadata the row shows (workspace path and name, model, provider, profile, source label), then message content.
|
|
630
926
|
*/
|
|
631
|
-
sidebarSearch(q, params, opts) {
|
|
632
|
-
let rows = this.list(params).body.sessions;
|
|
927
|
+
async sidebarSearch(q, params, opts) {
|
|
928
|
+
let rows = (await this.list(params)).body.sessions;
|
|
633
929
|
if (opts.projectId === 'none')
|
|
634
930
|
rows = rows.filter((r) => !r.project_id);
|
|
635
931
|
else if (opts.projectId)
|
|
@@ -638,22 +934,23 @@ export class SessionService {
|
|
|
638
934
|
const base = { all_profiles: params.allProfiles, active_profile: this.deps.activeProfile(), include_archived: params.includeArchived, sidebar_filtered: true };
|
|
639
935
|
if (!query)
|
|
640
936
|
return { ...base, sessions: rows };
|
|
937
|
+
const terms = sessionSearchTerms(query);
|
|
641
938
|
const results = [];
|
|
642
939
|
for (const row of rows) {
|
|
643
|
-
if (str(row.title)
|
|
940
|
+
if (sessionSearchMatches(str(row.title), terms))
|
|
644
941
|
results.push({ ...row, match_type: 'title' });
|
|
645
|
-
else if (['workspace', 'workspace_name', 'model', 'model_provider', 'profile', 'source_label'].some((k) => str(row[k])
|
|
942
|
+
else if (['workspace', 'workspace_name', 'model', 'model_provider', 'profile', 'source_label'].some((k) => sessionSearchMatches(str(row[k]), terms)))
|
|
646
943
|
results.push({ ...row, match_type: 'metadata' });
|
|
647
944
|
else if (opts.content) {
|
|
648
|
-
const hit = this.contentMatch(str(row.session_id),
|
|
945
|
+
const hit = this.contentMatch(str(row.session_id), terms, opts.depth);
|
|
649
946
|
if (hit)
|
|
650
947
|
results.push({ ...row, ...hit });
|
|
651
948
|
}
|
|
652
949
|
}
|
|
653
950
|
return { ...base, sessions: results, query, count: results.length };
|
|
654
951
|
}
|
|
655
|
-
/** The first of a stored session's first `depth` messages (0: all) containing
|
|
656
|
-
contentMatch(sid,
|
|
952
|
+
/** The first of a stored session's first `depth` messages (0: all) containing every term, with its redacted excerpt. */
|
|
953
|
+
contentMatch(sid, terms, depth) {
|
|
657
954
|
let sess;
|
|
658
955
|
try {
|
|
659
956
|
sess = this.store.get(sid, { promote: false, cacheOnMiss: false });
|
|
@@ -663,10 +960,10 @@ export class SessionService {
|
|
|
663
960
|
}
|
|
664
961
|
for (const m of depth ? sess.messages.slice(0, depth) : sess.messages) {
|
|
665
962
|
const c = sessionSearchMessageText(m);
|
|
666
|
-
if (!c
|
|
963
|
+
if (!sessionSearchMatches(c, terms))
|
|
667
964
|
continue;
|
|
668
965
|
const item = { match_type: 'content' };
|
|
669
|
-
const preview = sessionSearchPreview(c,
|
|
966
|
+
const preview = sessionSearchPreview(c, terms);
|
|
670
967
|
if (preview)
|
|
671
968
|
item.match_preview = redactText(preview, this.deps.redactEnabled());
|
|
672
969
|
return item;
|
|
@@ -721,14 +1018,16 @@ export class SessionService {
|
|
|
721
1018
|
const [workspace] = this.deps.workspaces.resolveImplicitWithRecovery(value, (p) => this.deps.workspaces.profileDefaultWorkspaceForBoot(p), profile);
|
|
722
1019
|
return workspace;
|
|
723
1020
|
}
|
|
1021
|
+
/** The server owns the names' normalization (TAL-631): trimmed, blanks dropped, and nothing left is the profile's defaults. */
|
|
724
1022
|
validateToolsetsShape(toolsets) {
|
|
725
1023
|
if (toolsets === null || toolsets === undefined)
|
|
726
1024
|
return null;
|
|
727
|
-
if (!Array.isArray(toolsets)
|
|
728
|
-
throw new HttpFailure(400, 'toolsets must be a
|
|
729
|
-
if (!toolsets.every((t) => typeof t === 'string'
|
|
730
|
-
throw new HttpFailure(400, 'each toolset must be a
|
|
731
|
-
|
|
1025
|
+
if (!Array.isArray(toolsets))
|
|
1026
|
+
throw new HttpFailure(400, 'toolsets must be a list or null');
|
|
1027
|
+
if (!toolsets.every((t) => typeof t === 'string'))
|
|
1028
|
+
throw new HttpFailure(400, 'each toolset must be a string');
|
|
1029
|
+
const names = toolsets.map((t) => t.trim()).filter(Boolean);
|
|
1030
|
+
return names.length ? names : null;
|
|
732
1031
|
}
|
|
733
1032
|
create(body, opts = {}) {
|
|
734
1033
|
// Python `new_session`: a body without a profile takes the request's active profile (cookie first).
|
|
@@ -771,7 +1070,8 @@ export class SessionService {
|
|
|
771
1070
|
markSessionTitleGenerated(current);
|
|
772
1071
|
this.store.save(current, { touchUpdatedAt: false });
|
|
773
1072
|
});
|
|
774
|
-
|
|
1073
|
+
// An explicit regenerate is user-requested, so state.db takes it over a manual name.
|
|
1074
|
+
await this.deps.syncTitle(current, true);
|
|
775
1075
|
this.publish(eventReason, current.profile, current.session_id);
|
|
776
1076
|
return current;
|
|
777
1077
|
}
|
|
@@ -781,7 +1081,7 @@ export class SessionService {
|
|
|
781
1081
|
applySessionTitleRename(s, rawTitle);
|
|
782
1082
|
this.store.save(s);
|
|
783
1083
|
});
|
|
784
|
-
await this.deps.syncTitle(s);
|
|
1084
|
+
await this.deps.syncTitle(s, true);
|
|
785
1085
|
this.publish('session_rename', s.profile, s.session_id);
|
|
786
1086
|
return { session: this.wireRow(s) };
|
|
787
1087
|
}
|
|
@@ -923,6 +1223,7 @@ export class SessionService {
|
|
|
923
1223
|
throw new HttpFailure(400, 'keep_count must be non-negative');
|
|
924
1224
|
await this.store.withLock(sid, () => {
|
|
925
1225
|
truncateSessionAtKeep(s, keep);
|
|
1226
|
+
this.markStateDbSeen(s, undefined, { boundary: true });
|
|
926
1227
|
this.store.save(s);
|
|
927
1228
|
});
|
|
928
1229
|
this.deps.runtime.evictAgent(sid);
|
|
@@ -967,12 +1268,146 @@ export class SessionService {
|
|
|
967
1268
|
compressionTarget(sid) {
|
|
968
1269
|
this.rejectSubagent(sid, 'compressed');
|
|
969
1270
|
const s = this.mutationTarget(sid, 'compressed');
|
|
1271
|
+
return { s, history: this.compressionHistory(s) };
|
|
1272
|
+
}
|
|
1273
|
+
/** The loaded session's own compression guards: no running turn and enough model history. */
|
|
1274
|
+
compressionHistory(s, stateRows) {
|
|
970
1275
|
if (s.active_stream_id)
|
|
971
1276
|
throw new HttpFailure(409, 'Session is still streaming; wait for the current turn to finish.');
|
|
972
|
-
const history = sanitizeMessagesForApi(this.modelContext(s));
|
|
1277
|
+
const history = sanitizeMessagesForApi(this.modelContext(s, stateRows));
|
|
973
1278
|
if (history.length < 4)
|
|
974
1279
|
throw new HttpFailure(400, 'Not enough conversation to compress (need at least 4 messages).');
|
|
975
|
-
return
|
|
1280
|
+
return history;
|
|
1281
|
+
}
|
|
1282
|
+
/** TAL-614: whether a manual compression would start now: `compressionTarget`'s gates, with `readOnly` the wire flag. */
|
|
1283
|
+
canCompress(s, readOnly, stateRows) {
|
|
1284
|
+
if (readOnly || isClaudeCodeSessionId(s.session_id))
|
|
1285
|
+
return false;
|
|
1286
|
+
try {
|
|
1287
|
+
this.compressionHistory(s, stateRows);
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
catch (error) {
|
|
1291
|
+
if (error instanceof HttpFailure)
|
|
1292
|
+
return false;
|
|
1293
|
+
throw error;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
/** TAL-258: the session's WebUI record (absent for a CLI or gateway session) and the profile whose state.db holds its rows. */
|
|
1297
|
+
handoffTarget(sid) {
|
|
1298
|
+
let local = null;
|
|
1299
|
+
try {
|
|
1300
|
+
local = this.store.get(sid);
|
|
1301
|
+
}
|
|
1302
|
+
catch { /* a foreign session without a WebUI record */ }
|
|
1303
|
+
const profile = local?.profile ?? this.deps.activeProfile();
|
|
1304
|
+
return { local, profile, dbPath: this.stateDbPath(profile) };
|
|
1305
|
+
}
|
|
1306
|
+
/** Python `_handle_conversation_rounds` (TAL-258): the session's rounds and whether the handoff dock is due. */
|
|
1307
|
+
conversationRounds(sid, since) {
|
|
1308
|
+
const rounds = countConversationRounds(this.handoffTarget(sid).dbPath, sid, since);
|
|
1309
|
+
return { ok: true, rounds, threshold: CONVERSATION_ROUND_THRESHOLD, should_show: rounds >= CONVERSATION_ROUND_THRESHOLD };
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Python `_handle_handoff_summary` (TAL-258): the session's main model summarizes its last 50 state.db messages after
|
|
1313
|
+
* `since`, and the summary is appended as a display-only `handoff_summary` tool row. A failed or cut-off answer falls
|
|
1314
|
+
* back to a deterministic local summary rather than an error.
|
|
1315
|
+
*/
|
|
1316
|
+
async handoffSummary(sid, since) {
|
|
1317
|
+
this.rejectSubagent(sid, 'summarized');
|
|
1318
|
+
const { local, profile, dbPath } = this.handoffTarget(sid);
|
|
1319
|
+
const rounds = countConversationRounds(dbPath, sid, since);
|
|
1320
|
+
if (rounds < CONVERSATION_ROUND_THRESHOLD)
|
|
1321
|
+
throw new HttpFailure(400, 'Not enough conversation rounds to generate a summary.');
|
|
1322
|
+
// A running turn settles the transcript it started from, which would drop a card appended meanwhile.
|
|
1323
|
+
if (local?.active_stream_id)
|
|
1324
|
+
throw new HttpFailure(409, 'Session is still streaming; wait for the current turn to finish.');
|
|
1325
|
+
const all = stateDbSessionMessages(dbPath, sid, { stitch: true });
|
|
1326
|
+
const messages = (since === null ? all : all.filter((m) => { const ts = stateDbTimestampSeconds(m.timestamp); return ts !== null && ts > since; })).slice(-50);
|
|
1327
|
+
if (messages.length < 2)
|
|
1328
|
+
throw new HttpFailure(400, 'Not enough messages to summarize.');
|
|
1329
|
+
const sidecar = this.deps.sidecar?.() ?? null;
|
|
1330
|
+
await ensureAgentRuntimeCurrent(sidecar);
|
|
1331
|
+
const meta = local ? null : this.lookupCliMeta(sid);
|
|
1332
|
+
const generated = await this.generateHandoffSummary(sidecar, profile, local, meta, messages);
|
|
1333
|
+
const pick = (row, ...keys) => { for (const key of keys) {
|
|
1334
|
+
const v = str(row?.[key]).trim();
|
|
1335
|
+
if (v)
|
|
1336
|
+
return v;
|
|
1337
|
+
} return ''; };
|
|
1338
|
+
// Python `get_cli_sessions` never listed WebUI's own state.db rows, so they name no channel.
|
|
1339
|
+
const cliRow = meta ?? this.lookupCliMeta(sid);
|
|
1340
|
+
const channel = pick(local, 'source_label', 'raw_source', 'source_tag', 'session_source') || (str(cliRow?.raw_source || cliRow?.source).trim().toLowerCase() === 'webui' ? '' : pick(cliRow, 'source_label', 'raw_source', 'source_tag', 'source')) || null;
|
|
1341
|
+
await this.persistHandoffMarker(sid, profile, handoffMarker(sid, generated.summary, channel, rounds, generated.fallback, this.deps.now()));
|
|
1342
|
+
return { ok: true, summary: generated.summary, message_count: messages.length, rounds, fallback: generated.fallback, ...(generated.warning ? { warning: generated.warning } : {}) };
|
|
1343
|
+
}
|
|
1344
|
+
/** The main model's summary at 700 tokens, once more at 1400 when it was cut off, else the local fallback. */
|
|
1345
|
+
async generateHandoffSummary(sidecar, profile, local, meta, messages) {
|
|
1346
|
+
const fallback = (warning) => ({ summary: fallbackHandoffSummary(messages), fallback: true, ...(warning ? { warning: `Summary generation used local fallback: ${warning}` } : {}) });
|
|
1347
|
+
if (!sidecar)
|
|
1348
|
+
return fallback('the Agent sidecar is not running');
|
|
1349
|
+
const [model, provider] = local ? this.deps.modelStateFromRequest(local.model, local.model_provider, local.model_provider) : [str(meta?.model).trim() || null, null];
|
|
1350
|
+
const prompt = [{ role: 'system', content: HANDOFF_SYSTEM_PROMPT }, { role: 'user', content: `Conversation transcript:\n${handoffTranscript(messages)}` }];
|
|
1351
|
+
const ask = (maxTokens) => sidecar.call('aux.complete', { profile_home: this.deps.profileHome(profile), task: 'handoff_summary', messages: prompt, model: model ?? '', provider, max_tokens: maxTokens, temperature: 0.2 }, { timeoutMs: 60_000 });
|
|
1352
|
+
try {
|
|
1353
|
+
let result = await ask(700);
|
|
1354
|
+
if (completionIncomplete(result))
|
|
1355
|
+
result = await ask(1400);
|
|
1356
|
+
return completionIncomplete(result) ? fallback() : { summary: result.text, fallback: false };
|
|
1357
|
+
}
|
|
1358
|
+
catch (error) {
|
|
1359
|
+
if (error instanceof SidecarError && error.condition === 'agent_runtime_stale')
|
|
1360
|
+
throw staleRuntimeFailure(error);
|
|
1361
|
+
// No credential is not a failure: the predecessor answered with the local summary and no warning.
|
|
1362
|
+
if (error instanceof SidecarError && error.condition === 'credential_missing')
|
|
1363
|
+
return fallback();
|
|
1364
|
+
return fallback(sanitizePaths(error));
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Python `_persist_handoff_summary`: a messaging session gets the marker in state.db and its WebUI record; any other
|
|
1369
|
+
* session in its WebUI record, or in state.db when it has none. Each store skips a marker its tail already holds, and
|
|
1370
|
+
* the session lock keeps two summaries from both passing that check. A card neither store saved answers 503.
|
|
1371
|
+
*/
|
|
1372
|
+
async persistHandoffMarker(sid, profile, marker) {
|
|
1373
|
+
const card = messageHandoffPayload(marker);
|
|
1374
|
+
const toStateDb = async () => {
|
|
1375
|
+
if (sameHandoff(handoffPayload(stateDbTailToolContent(this.stateDbPath(profile), sid)), card))
|
|
1376
|
+
return true;
|
|
1377
|
+
const sidecar = this.deps.sidecar?.();
|
|
1378
|
+
if (!sidecar)
|
|
1379
|
+
return false;
|
|
1380
|
+
try {
|
|
1381
|
+
return (await sidecar.call('state_db.append_message', { profile_home: this.deps.profileHome(profile), session_id: sid, role: 'tool', content: str(marker.content), tool_name: 'handoff_summary', timestamp: Number(marker.timestamp) })).ok;
|
|
1382
|
+
}
|
|
1383
|
+
catch {
|
|
1384
|
+
return false;
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
await this.store.withLock(sid, async () => {
|
|
1388
|
+
let live = null;
|
|
1389
|
+
try {
|
|
1390
|
+
live = this.store.get(sid);
|
|
1391
|
+
}
|
|
1392
|
+
catch { /* no WebUI record */ }
|
|
1393
|
+
// A turn that started while the summary was generated would drop the card when it settles.
|
|
1394
|
+
if (live?.active_stream_id)
|
|
1395
|
+
throw new HttpFailure(409, 'Session is still streaming; wait for the current turn to finish.');
|
|
1396
|
+
const toLocal = () => {
|
|
1397
|
+
if (!live)
|
|
1398
|
+
return false;
|
|
1399
|
+
if (sameHandoff(messageHandoffPayload(live.messages.at(-1)), card))
|
|
1400
|
+
return true;
|
|
1401
|
+
live.messages.push(marker);
|
|
1402
|
+
this.store.save(live);
|
|
1403
|
+
return true;
|
|
1404
|
+
};
|
|
1405
|
+
const messaging = this.isMessagingSession(sid);
|
|
1406
|
+
const saved = messaging ? [await toStateDb(), toLocal()].some(Boolean) : toLocal() || await toStateDb();
|
|
1407
|
+
// The caller may switch away on success, so a card saved nowhere is an error, not a summary.
|
|
1408
|
+
if (!saved)
|
|
1409
|
+
throw new HttpFailure(503, 'The handoff summary could not be saved; please retry.');
|
|
1410
|
+
});
|
|
976
1411
|
}
|
|
977
1412
|
/** The session's compression job; a deleted session's finished result is dropped rather than served. */
|
|
978
1413
|
compressionJob(sid) {
|
|
@@ -1034,7 +1469,8 @@ export class SessionService {
|
|
|
1034
1469
|
if (streamState(live) !== streamBefore)
|
|
1035
1470
|
throw new HttpFailure(409, 'Session stream state changed during compression; please retry.');
|
|
1036
1471
|
// One state.db read serves the check, the kept display rows, and the boundary.
|
|
1037
|
-
const
|
|
1472
|
+
const read = this.stateDbRead(live);
|
|
1473
|
+
const stateRows = read.rows;
|
|
1038
1474
|
if (transcriptKey(live, sanitizeMessagesForApi(this.modelContext(live, stateRows))) !== historyKey)
|
|
1039
1475
|
throw new HttpFailure(409, 'Session was modified during compression; please retry.');
|
|
1040
1476
|
// Rows the sidecar returns without a timestamp take the newest one this compression saw, never the clock: the
|
|
@@ -1065,6 +1501,7 @@ export class SessionService {
|
|
|
1065
1501
|
live.truncation_watermark = truncationWatermarkFor(compressed);
|
|
1066
1502
|
live.truncation_boundary = live.truncation_watermark;
|
|
1067
1503
|
live.truncation_watermark_compressed = true;
|
|
1504
|
+
this.markStateDbSeen(live, read, { boundary: true });
|
|
1068
1505
|
live.last_prompt_tokens = result.after_tokens;
|
|
1069
1506
|
live.post_compression_context_tokens_estimate = result.after_tokens;
|
|
1070
1507
|
this.store.save(live);
|
|
@@ -1126,6 +1563,7 @@ export class SessionService {
|
|
|
1126
1563
|
s.pending_user_source = null;
|
|
1127
1564
|
s.clear_generation = hadMessages ? randomUUID().replace(/-/g, '') : null;
|
|
1128
1565
|
applySessionTitleRename(s, 'Untitled');
|
|
1566
|
+
this.markStateDbSeen(s, undefined, { boundary: true });
|
|
1129
1567
|
this.store.save(s);
|
|
1130
1568
|
if (hadMessages) {
|
|
1131
1569
|
try {
|
|
@@ -1156,6 +1594,7 @@ export class SessionService {
|
|
|
1156
1594
|
return { error: 'The last message has nothing to resend.' };
|
|
1157
1595
|
const removed = history.length - lastUser;
|
|
1158
1596
|
shrinkTo(s, lastUser);
|
|
1597
|
+
this.markStateDbSeen(s, undefined, { boundary: true });
|
|
1159
1598
|
this.store.save(s);
|
|
1160
1599
|
return { ok: true, last_user_text: lastUserText, last_user_prompt: lastUserPrompt, last_user_attachments: lastUserAttachments, removed_count: removed };
|
|
1161
1600
|
});
|
|
@@ -1171,6 +1610,7 @@ export class SessionService {
|
|
|
1171
1610
|
const removedText = extractText(history[lastUser]?.content);
|
|
1172
1611
|
const removed = history.length - lastUser;
|
|
1173
1612
|
shrinkTo(s, lastUser);
|
|
1613
|
+
this.markStateDbSeen(s, undefined, { boundary: true });
|
|
1174
1614
|
this.store.save(s);
|
|
1175
1615
|
const preview = removedText.length > 40 ? `${removedText.slice(0, 40)}...` : removedText;
|
|
1176
1616
|
return { ok: true, removed_count: removed, removed_preview: preview };
|
|
@@ -1246,14 +1686,109 @@ export class SessionService {
|
|
|
1246
1686
|
}
|
|
1247
1687
|
return { session_id: branch.session_id, title, parent_session_id: source.session_id };
|
|
1248
1688
|
}
|
|
1689
|
+
/** Python `read_session_lineage_report` on the active profile's state.db. */
|
|
1690
|
+
lineageReport(sid) {
|
|
1691
|
+
return stateDbLineageReport(this.stateDbPath(null), sid);
|
|
1692
|
+
}
|
|
1693
|
+
/** TAL-259: recovery reads the active profile's state.db, and a recovered sidecar joins that profile. */
|
|
1694
|
+
recoveryDeps() {
|
|
1695
|
+
return { store: this.store, stateDbPath: this.stateDbPath(null), profile: this.deps.activeProfile(), stampingSince: this.deps.recoveryStampingSince(), log: this.deps.log };
|
|
1696
|
+
}
|
|
1697
|
+
recoveryAudit() {
|
|
1698
|
+
return auditSessionRecovery(this.recoveryDeps());
|
|
1699
|
+
}
|
|
1700
|
+
recoveryRepairSafe() {
|
|
1701
|
+
return repairSafeSessionRecovery(this.recoveryDeps());
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Python `_handle_session_compression_recovery_start`: open a compression-exhausted session's focused continuation, an
|
|
1705
|
+
* empty fork keeping its workspace, model, profile, toolsets and worktree. A retry opens the oldest continuation
|
|
1706
|
+
* already started from it in the same profile; the source's lock makes concurrent starts converge on one child.
|
|
1707
|
+
*/
|
|
1708
|
+
async compressionRecoveryStart(sid) {
|
|
1709
|
+
if (this.isSubagentViewOnly(sid))
|
|
1710
|
+
throw new HttpFailure(400, 'Subagent sessions are view-only and cannot start compression recovery from WebUI');
|
|
1711
|
+
const action = COMPRESSION_RECOVERY_ACTION_START_FOCUSED;
|
|
1712
|
+
let created = false;
|
|
1713
|
+
const child = await this.store.withLock(sid, () => {
|
|
1714
|
+
// Validated under the lock: a delete that won it leaves no source to fork.
|
|
1715
|
+
const source = this.store.get(sid);
|
|
1716
|
+
if (!this.visibleToActiveProfile(source.profile))
|
|
1717
|
+
throw new HttpFailure(404, 'Session not found');
|
|
1718
|
+
if (!compressionRecoveryPayload(source))
|
|
1719
|
+
throw new HttpFailure(409, 'Session does not have a compression recovery action.');
|
|
1720
|
+
const existing = this.compressionRecoveryChild(sid, action, source.profile);
|
|
1721
|
+
if (existing)
|
|
1722
|
+
return existing;
|
|
1723
|
+
const base = (source.title || 'Untitled').trim() || 'Untitled';
|
|
1724
|
+
const fresh = new Session({
|
|
1725
|
+
title: base.endsWith(' (focused continuation)') ? base : `${base} (focused continuation)`, workspace: source.workspace, model: source.model, model_provider: source.model_provider,
|
|
1726
|
+
project_id: source.project_id, profile: source.profile, session_source: 'fork', personality: source.personality, enabled_toolsets: copyJson(source.enabled_toolsets),
|
|
1727
|
+
context_length: source.context_length, threshold_tokens: source.threshold_tokens, gateway_routing: copyJson(source.gateway_routing), gateway_routing_history: copyJson(source.gateway_routing_history),
|
|
1728
|
+
parent_session_id: source.session_id, worktree_path: source.worktree_path, worktree_branch: source.worktree_branch, worktree_repo_root: source.worktree_repo_root, worktree_created_at: source.worktree_created_at,
|
|
1729
|
+
compression_recovery_source_session_id: sid, compression_recovery_action: action,
|
|
1730
|
+
// An empty model-facing transcript: the focused follow-up must not replay the exhausted context.
|
|
1731
|
+
messages: [], context_messages: [], composer_draft: { text: '', files: [] },
|
|
1732
|
+
}, { workspace: source.workspace, model: source.model });
|
|
1733
|
+
try {
|
|
1734
|
+
this.store.save(fresh);
|
|
1735
|
+
}
|
|
1736
|
+
catch (error) {
|
|
1737
|
+
throw new HttpFailure(500, `Failed to start compression recovery: ${sanitizePaths(error)}`);
|
|
1738
|
+
}
|
|
1739
|
+
this.store.touch(fresh);
|
|
1740
|
+
created = true;
|
|
1741
|
+
return fresh;
|
|
1742
|
+
});
|
|
1743
|
+
if (created)
|
|
1744
|
+
this.publish('session_compression_recovery', child.profile, child.session_id);
|
|
1745
|
+
return {
|
|
1746
|
+
ok: true, session: this.publicSession(child), source_session_id: sid, recommended_recovery_action: action,
|
|
1747
|
+
message: created ? 'Started a focused continuation. Describe the next narrow task to continue.' : 'Opened the existing focused continuation for this exhausted session.',
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
/** Python `find_compression_recovery_session`: the oldest same-profile continuation started from `sid`, cached or persisted. */
|
|
1751
|
+
compressionRecoveryChild(sid, action, profile) {
|
|
1752
|
+
const matches = (s) => s.compression_recovery_source_session_id === sid && s.compression_recovery_action === action && this.deps.profilesMatch(s.profile, profile);
|
|
1753
|
+
const found = new Map();
|
|
1754
|
+
for (const s of this.store.sessions.values())
|
|
1755
|
+
if (matches(s))
|
|
1756
|
+
found.set(s.session_id, s);
|
|
1757
|
+
for (const id of this.store.persistedIds()) {
|
|
1758
|
+
if (found.has(id) || this.store.sessions.has(id))
|
|
1759
|
+
continue;
|
|
1760
|
+
try {
|
|
1761
|
+
const meta = this.store.get(id, { metadataOnly: true, promote: false, cacheOnMiss: false });
|
|
1762
|
+
if (matches(meta))
|
|
1763
|
+
found.set(id, meta);
|
|
1764
|
+
}
|
|
1765
|
+
catch { /* unreadable: not a match */ }
|
|
1766
|
+
}
|
|
1767
|
+
const [oldest] = [...found.values()].sort((a, b) => (a.created_at || 0) - (b.created_at || 0) || (a.updated_at || 0) - (b.updated_at || 0) || (a.session_id < b.session_id ? -1 : 1));
|
|
1768
|
+
return oldest ? this.store.get(oldest.session_id) : null;
|
|
1769
|
+
}
|
|
1770
|
+
/** The run that keeps `delete` answering 409, if any; stream status reports it so a draining client can wait on it (TAL-622). */
|
|
1771
|
+
activeRunBlocking(sid) {
|
|
1772
|
+
const live = this.deps.runtime.activeRunStream(sid);
|
|
1773
|
+
if (live)
|
|
1774
|
+
return live;
|
|
1775
|
+
try {
|
|
1776
|
+
const candidate = str(this.store.get(sid, { metadataOnly: true, promote: false, cacheOnMiss: false }).active_stream_id).trim();
|
|
1777
|
+
if (candidate && this.deps.runtime.activeStreamIds.has(candidate))
|
|
1778
|
+
return candidate;
|
|
1779
|
+
}
|
|
1780
|
+
catch { /* absent */ }
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1249
1783
|
async delete(sid) {
|
|
1250
1784
|
if (!sid)
|
|
1251
1785
|
throw new HttpFailure(400, 'session_id is required');
|
|
1252
1786
|
if (!isSafeSessionId(sid))
|
|
1253
1787
|
throw new HttpFailure(400, 'Invalid session_id');
|
|
1254
1788
|
// Python: a read-only import (persisted sidecar flag, sidebar metadata, or a foreign state.db owner such as a
|
|
1255
|
-
// Claude Code session) is never deleted — that would erase the owner's authoritative transcript.
|
|
1256
|
-
|
|
1789
|
+
// Claude Code session) is never deleted — that would erase the owner's authoritative transcript. An orphan has none.
|
|
1790
|
+
const orphaned = this.isOrphanedImport(sid);
|
|
1791
|
+
if (!orphaned && this.isReadOnlyImport(sid))
|
|
1257
1792
|
throw new HttpFailure(400, 'Read-only imported sessions cannot be deleted from WebUI');
|
|
1258
1793
|
if (this.isSubagentViewOnly(sid))
|
|
1259
1794
|
throw new HttpFailure(400, 'Subagent sessions are view-only and cannot be deleted from WebUI');
|
|
@@ -1273,29 +1808,12 @@ export class SessionService {
|
|
|
1273
1808
|
hadSidecar = false;
|
|
1274
1809
|
}
|
|
1275
1810
|
// Python `_is_messaging_session_id`: decided before the JSON is gone, from WebUI metadata or the Agent's row.
|
|
1276
|
-
const isMessaging = (
|
|
1277
|
-
|
|
1278
|
-
return true;
|
|
1279
|
-
}
|
|
1280
|
-
catch { /* absent */ } const meta = this.lookupCliMeta(sid); return meta !== null && isMessagingSessionRecord(meta); })();
|
|
1281
|
-
const blocking = () => {
|
|
1282
|
-
const live = this.deps.runtime.activeRunStream(sid);
|
|
1283
|
-
if (live)
|
|
1284
|
-
return live;
|
|
1285
|
-
try {
|
|
1286
|
-
const snapshot = this.store.get(sid, { metadataOnly: true });
|
|
1287
|
-
const candidate = str(snapshot.active_stream_id).trim();
|
|
1288
|
-
if (candidate && this.deps.runtime.activeStreamIds.has(candidate))
|
|
1289
|
-
return candidate;
|
|
1290
|
-
}
|
|
1291
|
-
catch { /* absent */ }
|
|
1292
|
-
return null;
|
|
1293
|
-
};
|
|
1294
|
-
if (blocking())
|
|
1811
|
+
const isMessaging = this.isMessagingSession(sid);
|
|
1812
|
+
if (this.activeRunBlocking(sid))
|
|
1295
1813
|
throw new HttpFailure(409, 'Session has an active run; stop it before deleting');
|
|
1296
1814
|
try {
|
|
1297
1815
|
await this.store.withLock(sid, () => {
|
|
1298
|
-
if (
|
|
1816
|
+
if (this.activeRunBlocking(sid))
|
|
1299
1817
|
throw new HttpFailure(409, 'Session has an active run; stop it before deleting');
|
|
1300
1818
|
// A public share outlives its session file, and revokeShare needs the session, so revoke it first.
|
|
1301
1819
|
try {
|
|
@@ -1328,13 +1846,24 @@ export class SessionService {
|
|
|
1328
1846
|
// Python: the Agent/CLI transcript in state.db goes too (else a claimable CLI row resurfaces in the sidebar), but a
|
|
1329
1847
|
// messaging channel's memory is never erased from the WebUI; the actual outcome is reported.
|
|
1330
1848
|
let stateDbCleanupFailed = false;
|
|
1331
|
-
if (!isMessaging) {
|
|
1849
|
+
if (!isMessaging && !orphaned) {
|
|
1850
|
+
// TAL-529: the compression continuations go too, tip first; the Agent's delete keeps and detaches them otherwise.
|
|
1851
|
+
let lineage = [];
|
|
1332
1852
|
try {
|
|
1333
|
-
|
|
1853
|
+
lineage = stateDbCompressionLineage(this.stateDbPath(eventProfile), sid);
|
|
1334
1854
|
}
|
|
1335
1855
|
catch {
|
|
1336
1856
|
stateDbCleanupFailed = true;
|
|
1337
1857
|
}
|
|
1858
|
+
for (const id of lineage.reverse()) {
|
|
1859
|
+
try {
|
|
1860
|
+
if (!(await this.deps.runtime.deleteCliSession(eventProfile, id)))
|
|
1861
|
+
stateDbCleanupFailed = true;
|
|
1862
|
+
}
|
|
1863
|
+
catch {
|
|
1864
|
+
stateDbCleanupFailed = true;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1338
1867
|
}
|
|
1339
1868
|
this.publish('session_delete', eventProfile);
|
|
1340
1869
|
// Only a sidecar session is ever published to the relay.
|
|
@@ -1603,6 +2132,7 @@ export class SessionService {
|
|
|
1603
2132
|
}
|
|
1604
2133
|
// ── shares ───────────────────────────────────────────────────────────────
|
|
1605
2134
|
createShare(sid) {
|
|
2135
|
+
this.rejectClaudeCode(sid, 'shared');
|
|
1606
2136
|
let s = this.get404(sid);
|
|
1607
2137
|
if (!this.visibleToActiveProfile(s.profile))
|
|
1608
2138
|
throw new HttpFailure(404, 'Session not found');
|
|
@@ -1621,7 +2151,7 @@ export class SessionService {
|
|
|
1621
2151
|
this.publish('session_share_create', s.profile, sid);
|
|
1622
2152
|
return {
|
|
1623
2153
|
ok: true,
|
|
1624
|
-
share: { token: meta.share_token, url:
|
|
2154
|
+
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 },
|
|
1625
2155
|
session: this.publicSession(s),
|
|
1626
2156
|
};
|
|
1627
2157
|
}
|
|
@@ -1688,6 +2218,44 @@ function findLastUserIndex(history) {
|
|
|
1688
2218
|
export function sanitizePaths(error) {
|
|
1689
2219
|
return str(error?.message ?? error).replace(/(?:(?:\/[a-zA-Z0-9_.-]+)+|(?:[A-Z]:\\[^\s]+))/g, '<path>');
|
|
1690
2220
|
}
|
|
2221
|
+
/** 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). */
|
|
2222
|
+
function runStateDbRows(rows, prompt) {
|
|
2223
|
+
const at = rows.findIndex((m) => looksLikeCurrentUserTurn(m, prompt));
|
|
2224
|
+
if (at < 0)
|
|
2225
|
+
return null;
|
|
2226
|
+
const toolResult = (m) => Array.isArray(m.content) && m.content.some((part) => isDict(part) && part.type === 'tool_result');
|
|
2227
|
+
const end = rows.findIndex((m, i) => i > at && m.role === 'user' && !toolResult(m) && agentSteerText(m) === null);
|
|
2228
|
+
return rows.slice(at, end < 0 ? undefined : end);
|
|
2229
|
+
}
|
|
2230
|
+
/** TAL-493: a row's full-text identity; one with no text (reasoning or media only) by its raw role, content, and reasoning. */
|
|
2231
|
+
function settledIdentity(m) {
|
|
2232
|
+
return messageIdentity(m, Infinity) ?? JSON.stringify([m.role ?? null, m.content ?? null, reasoningFieldsText(m)]);
|
|
2233
|
+
}
|
|
2234
|
+
/** `rows` minus one occurrence of each row of `base`, by `settledIdentity`. */
|
|
2235
|
+
function withoutRows(rows, base) {
|
|
2236
|
+
const left = new Map();
|
|
2237
|
+
for (const m of base)
|
|
2238
|
+
left.set(settledIdentity(m), (left.get(settledIdentity(m)) ?? 0) + 1);
|
|
2239
|
+
return rows.filter((m) => { const key = settledIdentity(m); const n = left.get(key) ?? 0; if (n > 0)
|
|
2240
|
+
left.set(key, n - 1); return n === 0; });
|
|
2241
|
+
}
|
|
2242
|
+
/**
|
|
2243
|
+
* TAL-493: what the state.db marker was recorded under: the boundary fields every boundary writer sets (an older release
|
|
2244
|
+
* moves them without the marker) and the role, timestamp, and database file identity of the row the marker names (a
|
|
2245
|
+
* recreated state.db, whose ids start over, has no such row).
|
|
2246
|
+
*/
|
|
2247
|
+
function stateDbMarkKey(s, stateRows, seenId) {
|
|
2248
|
+
const anchor = seenId === null ? undefined : stateRows.find((m) => m._state_db_row_id === seenId);
|
|
2249
|
+
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]);
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* The recorded state.db marker while what it was recorded under still holds and this version wrote the session last (an
|
|
2253
|
+
* older release can apply a boundary that changes no field), else null (the timestamp rules apply).
|
|
2254
|
+
*/
|
|
2255
|
+
function currentStateDbSeenId(s, stateRows) {
|
|
2256
|
+
const seenId = s.state_db_seen_id;
|
|
2257
|
+
return seenId !== null && s.state_db_seen_stamp === s.updated_at && s.state_db_seen_boundary === stateDbMarkKey(s, stateRows, seenId) ? seenId : null;
|
|
2258
|
+
}
|
|
1691
2259
|
function truncationWatermarkFor(messages) {
|
|
1692
2260
|
if (!messages.length)
|
|
1693
2261
|
return 0.0;
|