@maudecode/talaria-web 1.18.0 → 1.19.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.js +16 -16
- package/dist/api/auth-raw.js.map +1 -1
- package/dist/api/automation-raw.d.ts +2 -2
- package/dist/api/automation-raw.js +32 -32
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +102 -22
- package/dist/api/automation-router.js +15 -13
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +1205 -34
- package/dist/api/chat-router.js +158 -78
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +2 -2
- package/dist/api/git-router.js +29 -29
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +3 -3
- package/dist/api/raw-routes.js +63 -61
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +6 -6
- package/dist/api/router.js +22 -22
- package/dist/api/router.js.map +1 -1
- package/dist/api/session-visibility.d.ts +4 -4
- package/dist/api/session-visibility.js +26 -16
- package/dist/api/session-visibility.js.map +1 -1
- package/dist/api/sessions-router.d.ts +1928 -124
- package/dist/api/sessions-router.js +177 -173
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +427 -9
- package/dist/api/settings-router.js +48 -32
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +2 -2
- package/dist/api/sse-routes.js +63 -63
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.d.ts +5 -0
- package/dist/api/tools-raw.js +58 -33
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +663 -91
- package/dist/api/tools-router.js +309 -52
- package/dist/api/tools-router.js.map +1 -1
- package/dist/api/transcribe-stream.d.ts +16 -0
- package/dist/api/transcribe-stream.js +172 -0
- package/dist/api/transcribe-stream.js.map +1 -0
- package/dist/app.d.ts +4 -1
- package/dist/app.js +66 -47
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +10 -9
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/passkeys.d.ts +15 -14
- package/dist/auth/passkeys.js +89 -91
- package/dist/auth/passkeys.js.map +1 -1
- package/dist/auth/store.d.ts +17 -7
- package/dist/auth/store.js +93 -52
- package/dist/auth/store.js.map +1 -1
- package/dist/bin/talaria-web.js +12 -81
- package/dist/bin/talaria-web.js.map +1 -1
- package/dist/cli/ctl.js +2 -1
- package/dist/cli/ctl.js.map +1 -1
- package/dist/cli/dotenv.d.ts +5 -0
- package/dist/cli/dotenv.js +14 -2
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/cli/launcher.js +5 -5
- package/dist/cli/launcher.js.map +1 -1
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +85 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/supervise.d.ts +2 -0
- package/dist/cli/supervise.js +67 -4
- package/dist/cli/supervise.js.map +1 -1
- package/dist/config/agent-config.d.ts +34 -11
- package/dist/config/agent-config.js +113 -58
- package/dist/config/agent-config.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +14 -5
- package/dist/config.js.map +1 -1
- package/dist/fs/atomic.d.ts +1 -0
- package/dist/fs/atomic.js +4 -1
- package/dist/fs/atomic.js.map +1 -1
- package/dist/fs/stat.d.ts +1 -0
- package/dist/fs/stat.js +11 -0
- package/dist/fs/stat.js.map +1 -0
- package/dist/http/context.d.ts +14 -9
- package/dist/http/context.js +29 -20
- package/dist/http/context.js.map +1 -1
- package/dist/onboarding.d.ts +2 -2
- package/dist/onboarding.js +10 -8
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.d.ts +10 -4
- package/dist/profiles/profiles.js +77 -30
- package/dist/profiles/profiles.js.map +1 -1
- package/dist/projects.d.ts +15 -6
- package/dist/projects.js +55 -47
- package/dist/projects.js.map +1 -1
- package/dist/providers/catalog.d.ts +78 -12
- package/dist/providers/catalog.js +445 -141
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/env-file.d.ts +10 -3
- package/dist/providers/env-file.js +24 -75
- package/dist/providers/env-file.js.map +1 -1
- package/dist/providers/runtime-env.d.ts +4 -2
- package/dist/providers/runtime-env.js +9 -6
- package/dist/providers/runtime-env.js.map +1 -1
- package/dist/providers/tables.d.ts +3 -1
- package/dist/providers/tables.js +4 -2
- package/dist/providers/tables.js.map +1 -1
- package/dist/redact.js +18 -9
- package/dist/redact.js.map +1 -1
- package/dist/release.d.ts +7 -0
- package/dist/release.js +14 -0
- package/dist/release.js.map +1 -1
- package/dist/runtime.d.ts +5 -1
- package/dist/runtime.js +233 -90
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +10 -0
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +9 -3
- package/dist/sessions/anchor.js +28 -36
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-events.d.ts +2 -0
- package/dist/sessions/background-events.js +10 -0
- package/dist/sessions/background-events.js.map +1 -0
- package/dist/sessions/background-tasks.d.ts +26 -20
- package/dist/sessions/background-tasks.js +88 -86
- package/dist/sessions/background-tasks.js.map +1 -1
- package/dist/sessions/background-updates.d.ts +15 -8
- package/dist/sessions/background-updates.js +101 -25
- package/dist/sessions/background-updates.js.map +1 -1
- package/dist/sessions/cli-sessions.d.ts +15 -5
- package/dist/sessions/cli-sessions.js +62 -43
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +30 -2
- package/dist/sessions/completions.js +294 -53
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/compress.d.ts +8 -0
- package/dist/sessions/compress.js +2 -2
- package/dist/sessions/compress.js.map +1 -1
- package/dist/sessions/drafts.d.ts +14 -10
- package/dist/sessions/drafts.js +38 -43
- package/dist/sessions/drafts.js.map +1 -1
- package/dist/sessions/gateway-sessions.d.ts +46 -0
- package/dist/sessions/gateway-sessions.js +378 -0
- package/dist/sessions/gateway-sessions.js.map +1 -0
- package/dist/sessions/goals.d.ts +31 -0
- package/dist/sessions/goals.js +75 -0
- package/dist/sessions/goals.js.map +1 -0
- package/dist/sessions/journal.d.ts +5 -79
- package/dist/sessions/journal.js +15 -606
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +27 -18
- package/dist/sessions/list.js +133 -131
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +68 -10
- package/dist/sessions/merge.js +259 -51
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/notices.d.ts +22 -0
- package/dist/sessions/notices.js +72 -0
- package/dist/sessions/notices.js.map +1 -0
- package/dist/sessions/pending.d.ts +10 -4
- package/dist/sessions/pending.js +83 -7
- package/dist/sessions/pending.js.map +1 -1
- package/dist/sessions/prefix-index.d.ts +82 -0
- package/dist/sessions/prefix-index.js +232 -0
- package/dist/sessions/prefix-index.js.map +1 -0
- package/dist/sessions/recovery.d.ts +4 -7
- package/dist/sessions/recovery.js +97 -281
- package/dist/sessions/recovery.js.map +1 -1
- package/dist/sessions/relay.d.ts +20 -20
- package/dist/sessions/relay.js +205 -127
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +162 -59
- package/dist/sessions/service.js +1133 -521
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +18 -2
- package/dist/sessions/session.js +29 -5
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/shares.d.ts +7 -6
- package/dist/sessions/shares.js +22 -45
- package/dist/sessions/shares.js.map +1 -1
- package/dist/sessions/source-kind.js +1 -0
- package/dist/sessions/source-kind.js.map +1 -1
- package/dist/sessions/state-db-identity.d.ts +18 -0
- package/dist/sessions/state-db-identity.js +67 -0
- package/dist/sessions/state-db-identity.js.map +1 -0
- package/dist/sessions/state-db-worker.js +3 -1
- package/dist/sessions/state-db-worker.js.map +1 -1
- package/dist/sessions/state-db.d.ts +53 -8
- package/dist/sessions/state-db.js +576 -50
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/storage/backend.d.ts +162 -0
- package/dist/sessions/storage/backend.js +19 -0
- package/dist/sessions/storage/backend.js.map +1 -0
- package/dist/sessions/storage/conformance.d.ts +20 -0
- package/dist/sessions/storage/conformance.js +237 -0
- package/dist/sessions/storage/conformance.js.map +1 -0
- package/dist/sessions/storage/document.d.ts +16 -0
- package/dist/sessions/storage/document.js +44 -0
- package/dist/sessions/storage/document.js.map +1 -0
- package/dist/sessions/storage/json-documents.d.ts +23 -0
- package/dist/sessions/storage/json-documents.js +233 -0
- package/dist/sessions/storage/json-documents.js.map +1 -0
- package/dist/sessions/storage/json-files.d.ts +12 -0
- package/dist/sessions/storage/json-files.js +124 -0
- package/dist/sessions/storage/json-files.js.map +1 -0
- package/dist/sessions/storage/json-installation.d.ts +3 -0
- package/dist/sessions/storage/json-installation.js +31 -0
- package/dist/sessions/storage/json-installation.js.map +1 -0
- package/dist/sessions/storage/json-journal.d.ts +114 -0
- package/dist/sessions/storage/json-journal.js +686 -0
- package/dist/sessions/storage/json-journal.js.map +1 -0
- package/dist/sessions/storage/json-recovery.d.ts +4 -0
- package/dist/sessions/storage/json-recovery.js +195 -0
- package/dist/sessions/storage/json-recovery.js.map +1 -0
- package/dist/sessions/storage/json.d.ts +68 -0
- package/dist/sessions/storage/json.js +258 -0
- package/dist/sessions/storage/json.js.map +1 -0
- package/dist/sessions/storage/row.d.ts +6 -0
- package/dist/sessions/storage/row.js +49 -0
- package/dist/sessions/storage/row.js.map +1 -0
- package/dist/sessions/storage/search-text.d.ts +4 -0
- package/dist/sessions/storage/search-text.js +16 -0
- package/dist/sessions/storage/search-text.js.map +1 -0
- package/dist/sessions/store.d.ts +63 -73
- package/dist/sessions/store.js +305 -611
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +4 -0
- package/dist/sessions/streams.js +9 -1
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/titles.js +1 -1
- package/dist/sessions/titles.js.map +1 -1
- package/dist/sessions/todo.d.ts +5 -0
- package/dist/sessions/todo.js +28 -21
- package/dist/sessions/todo.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +5 -4
- package/dist/sessions/tool-display.js +22 -16
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +10 -1
- package/dist/sessions/turn-context.js +7 -4
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +87 -17
- package/dist/sessions/turn.js +795 -347
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/wakeup-pause.d.ts +8 -13
- package/dist/sessions/wakeup-pause.js +31 -49
- package/dist/sessions/wakeup-pause.js.map +1 -1
- package/dist/sessions/window.js +13 -12
- package/dist/sessions/window.js.map +1 -1
- package/dist/settings.d.ts +19 -7
- package/dist/settings.js +179 -183
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/client.d.ts +17 -3
- package/dist/sidecar/client.js +78 -8
- package/dist/sidecar/client.js.map +1 -1
- package/dist/sidecar/discover.d.ts +2 -1
- package/dist/sidecar/discover.js +93 -10
- package/dist/sidecar/discover.js.map +1 -1
- package/dist/sidecar/env.d.ts +4 -0
- package/dist/sidecar/env.js +3 -0
- package/dist/sidecar/env.js.map +1 -0
- package/dist/sidecar/fake.js +19 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/test/env-file.d.ts +5 -0
- package/dist/test/env-file.js +88 -0
- package/dist/test/env-file.js.map +1 -0
- package/dist/test/harness.d.ts +1 -0
- package/dist/test/harness.js +1 -1
- package/dist/test/harness.js.map +1 -1
- package/dist/test/passkeys.d.ts +22 -0
- package/dist/test/passkeys.js +62 -0
- package/dist/test/passkeys.js.map +1 -0
- package/dist/test/storage.d.ts +15 -0
- package/dist/test/storage.js +26 -0
- package/dist/test/storage.js.map +1 -0
- package/dist/tools/commands.d.ts +6 -1
- package/dist/tools/commands.js +8 -2
- package/dist/tools/commands.js.map +1 -1
- package/dist/tools/connectors.d.ts +33 -0
- package/dist/tools/connectors.js +121 -0
- package/dist/tools/connectors.js.map +1 -0
- package/dist/tools/crons.d.ts +38 -1
- package/dist/tools/crons.js +82 -15
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +5 -5
- package/dist/tools/dashboard-plugins.js +14 -14
- package/dist/tools/dashboard-plugins.js.map +1 -1
- package/dist/tools/extensions.d.ts +5 -4
- package/dist/tools/extensions.js +213 -207
- package/dist/tools/extensions.js.map +1 -1
- package/dist/tools/gateway-restart.d.ts +62 -0
- package/dist/tools/gateway-restart.js +142 -0
- package/dist/tools/gateway-restart.js.map +1 -0
- package/dist/tools/health.d.ts +15 -3
- package/dist/tools/health.js +97 -34
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/hygiene.d.ts +4 -3
- package/dist/tools/hygiene.js +23 -9
- package/dist/tools/hygiene.js.map +1 -1
- package/dist/tools/kanban.d.ts +4 -2
- package/dist/tools/kanban.js +28 -19
- package/dist/tools/kanban.js.map +1 -1
- package/dist/tools/memory-masks.d.ts +5 -0
- package/dist/tools/memory-masks.js +256 -0
- package/dist/tools/memory-masks.js.map +1 -0
- package/dist/tools/memory.d.ts +29 -3
- package/dist/tools/memory.js +69 -27
- package/dist/tools/memory.js.map +1 -1
- package/dist/tools/plugin-settings.d.ts +15 -0
- package/dist/tools/plugin-settings.js +90 -0
- package/dist/tools/plugin-settings.js.map +1 -0
- package/dist/tools/skills.d.ts +9 -4
- package/dist/tools/skills.js +44 -13
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +34 -15
- package/dist/tools/update-notifications.js +141 -57
- package/dist/tools/update-notifications.js.map +1 -1
- package/dist/tools/update-rollback.d.ts +11 -0
- package/dist/tools/update-rollback.js +28 -0
- package/dist/tools/update-rollback.js.map +1 -0
- package/dist/tools/updates.d.ts +93 -33
- package/dist/tools/updates.js +1051 -345
- package/dist/tools/updates.js.map +1 -1
- package/dist/util.d.ts +2 -0
- package/dist/util.js +5 -0
- package/dist/util.js.map +1 -1
- package/dist/workspace/git.d.ts +16 -1
- package/dist/workspace/git.js +291 -11
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media.d.ts +12 -9
- package/dist/workspace/media.js +31 -20
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/rollback.d.ts +2 -2
- package/dist/workspace/rollback.js +5 -5
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/workspaces.d.ts +15 -19
- package/dist/workspace/workspaces.js +55 -127
- package/dist/workspace/workspaces.js.map +1 -1
- package/dist/workspace/worktrees.js +170 -32
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +5 -3
- package/sidecar/agent_dependency.json +4 -4
- package/sidecar/agent_support.json +26 -0
- package/sidecar/scripts/record-fixtures.py +12 -3
- package/sidecar/scripts/replay_sidecar.py +8 -0
- package/sidecar/scripts/suite_runner.py +302 -0
- package/sidecar/scripts/test.sh +12 -4
- package/sidecar/talaria_sidecar/__init__.py +6 -1
- package/sidecar/talaria_sidecar/__main__.py +4 -2
- package/sidecar/talaria_sidecar/agent_mutation.py +975 -0
- package/sidecar/talaria_sidecar/agent_services.py +1053 -0
- package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
- package/sidecar/talaria_sidecar/home.py +43 -1
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +139 -31
- package/sidecar/talaria_sidecar/methods/chat.py +847 -134
- package/sidecar/talaria_sidecar/methods/commands.py +213 -4
- package/sidecar/talaria_sidecar/methods/config.py +123 -4
- package/sidecar/talaria_sidecar/methods/connectors.py +343 -0
- package/sidecar/talaria_sidecar/methods/cron.py +366 -34
- package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
- package/sidecar/talaria_sidecar/methods/gateway.py +194 -54
- package/sidecar/talaria_sidecar/methods/gateway_sessions.py +234 -0
- package/sidecar/talaria_sidecar/methods/goals.py +81 -41
- package/sidecar/talaria_sidecar/methods/kanban.py +80 -27
- package/sidecar/talaria_sidecar/methods/memory.py +84 -0
- package/sidecar/talaria_sidecar/methods/oauth.py +5 -2
- package/sidecar/talaria_sidecar/methods/process.py +160 -11
- package/sidecar/talaria_sidecar/methods/profiles.py +44 -6
- package/sidecar/talaria_sidecar/methods/providers.py +196 -26
- package/sidecar/talaria_sidecar/methods/runtime.py +69 -1
- package/sidecar/talaria_sidecar/methods/skills.py +67 -4
- package/sidecar/talaria_sidecar/methods/state_db.py +277 -70
- package/sidecar/talaria_sidecar/methods/stt.py +159 -78
- package/sidecar/talaria_sidecar/methods/text.py +14 -1
- package/sidecar/talaria_sidecar/methods/toolsets.py +49 -0
- package/sidecar/talaria_sidecar/methods/usage.py +113 -14
- package/sidecar/talaria_sidecar/methods/worktree.py +26 -19
- package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
- package/sidecar/talaria_sidecar/rpc.py +45 -4
- package/sidecar/talaria_sidecar/runtime.py +81 -12
- package/sidecar/talaria_sidecar/speed.py +35 -0
- package/static/dist/FILES.txt +126 -127
- package/static/dist/assets/AppShell-cey33v9s.js +8 -0
- package/static/dist/assets/ChatPage-i8wnugr7.js +18 -0
- package/static/dist/assets/ConnectTargets-hg19553x.js +1 -0
- package/static/dist/assets/ExtensionRoute-cly4apm5.js +1 -0
- package/static/dist/assets/Field-j8uqoigz.js +9 -0
- package/static/dist/assets/HubRoute-e4bgz41j.js +2 -0
- package/static/dist/assets/Markdown-lahuichm.js +598 -0
- package/static/dist/assets/RightPanel-lbo4i0fn.js +1 -0
- package/static/dist/assets/Select-gqatm2e0.js +1 -0
- package/static/dist/assets/SessionListPanel-mfqayfjh.js +1 -0
- package/static/dist/assets/SettingsLayout-b8980u7k.js +1 -0
- package/static/dist/assets/States-e7dkgtxf.js +1 -0
- package/static/dist/assets/Toaster-bfqqy786.js +1 -0
- package/static/dist/assets/_app-n1n6pfx1.js +1 -0
- package/static/dist/assets/{_app.ext._extensionId-gfbzpoou.js → _app.ext._extensionId-buxjxav3.js} +1 -1
- package/static/dist/assets/_app.index-jt8zwiu2.js +1 -0
- package/static/dist/assets/_app.insights-cv76ml71.js +1 -0
- package/static/dist/assets/_app.kanban-h6ts53j7.js +1 -0
- package/static/dist/assets/_app.logs-dm873z90.js +1 -0
- package/static/dist/assets/_app.memory-glk5y6m8.js +1 -0
- package/static/dist/assets/_app.profiles-ka6zpstq.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-fn1bhde6.js → _app.session._sessionId-g2vzghvc.js} +1 -1
- package/static/dist/assets/_app.settings-hgjdhzcd.js +1 -0
- package/static/dist/assets/_app.settings._section-jf2kmhae.js +3 -0
- package/static/dist/assets/_app.skills-cii852yq.js +1 -0
- package/static/dist/assets/_app.tasks-fp9q7atm.js +1 -0
- package/static/dist/assets/_app.todos-dw94qzue.js +1 -0
- package/static/dist/assets/_app.workspaces-inyygzze.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-c8kdixke.js → abnfDiagram-VCTEODGH-gjayq3ra.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-lp56972r.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-cku06x1p.js → architectureDiagram-5GKGNRK7-nwb3xovn.js} +1 -1
- package/static/dist/assets/archive-b1j004vn.js +1 -0
- package/static/dist/assets/back-g74gkozu.js +1 -0
- package/static/dist/assets/{blockDiagram-I7D4REHJ-jzthp1wa.js → blockDiagram-I7D4REHJ-joedghi3.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-hyrjrohe.js} +1 -1
- package/static/dist/assets/channel-es95o4y8.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-koouqbut.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-bl4bhdnt.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-ofajdixh.js → chunk-DU6HZSFF-gpedttsp.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-d8xxi0zi.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-jro0hl73.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-djn6gbyu.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-vw4p5p7o.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-n1hj77q8.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-e7gkikcw.js → chunk-OSK3NFVY-oy0gwn2n.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-onf1hz6c.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-m8sqn7cs.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-ek4smc8x.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-g9gfvefo.js} +3 -3
- package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-f9nfv2cu.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.js → chunk-TICWLB2K-bnk3m1mq.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-kmihoqxh.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-kmihoqxh.js +1 -0
- package/static/dist/assets/copied-bli9tbcx.js +1 -0
- package/static/dist/assets/{copy-magtofyp.js → copy-jamtd1pf.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-siqlulkl.js → cynefin-OW5HDTMX-f8xfdd3f.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-i648tah9.js → cynefinDiagram-5FMLGOSQ-n9lb99h9.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-mwn6uc17.js → dagre-GXQ25YYZ-dgailddi.js} +4 -4
- package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-m6aeoe7c.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-mekq7zgq.js → diagram-UQ7AKVKN-jall3j0x.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-hbawi7hn.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-xe6r5qfw.js → diagram-VX7I27RA-hendl2tt.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-nq07dg7k.js → diagram-Z3DM3KII-igwc35p3.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-c62ipm3g.js → ebnfDiagram-PWID7BFC-c64knz08.js} +1 -1
- package/static/dist/assets/{erDiagram-RLTQ6QDP-h39kmnzy.js → erDiagram-RLTQ6QDP-brdu9lcm.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-njk1j8wl.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-nma6f0ga.js +1 -0
- package/static/dist/assets/folder-plus-lvt9kudb.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ngbsyp37.js → ganttDiagram-EL5Y4UJY-ma6jxj5i.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-fr2i7caa.js → gitGraph-4MIJSDKK-naa9w6xa.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-hzltvsod.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-jwpop9p8.js → highlighted-body-KPVGNVTW-ghoavj8d.js} +1 -1
- package/static/dist/assets/host-hzm8n85n.js +1 -0
- package/static/dist/assets/index-drdcaqeq.css +2 -0
- package/static/dist/assets/index-jzm844he.js +11 -0
- package/static/dist/assets/{info-A6RAGUB7-h11x0dxo.js → info-A6RAGUB7-mn2ut5ko.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-dfh47dw9.js → infoDiagram-27XIBGKW-h6kuqvsb.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-e7bnwq7h.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-etd1bi6p.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-emb1g9dz.js} +4 -4
- package/static/dist/assets/lazyRouteComponent-o816ytzl.js +1 -0
- package/static/dist/assets/{line-meizqsvn.js → line-h0ojzsvs.js} +1 -1
- package/static/dist/assets/link-k01ryaf9.js +1 -0
- package/static/dist/assets/login-bsae49ew.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-ihl34s74.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-ntne45i9.js → mermaid-parser.core-bnre27u8.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.js → mindmap-definition-YA3MSWOX-o1vathns.js} +1 -1
- package/static/dist/assets/onboarding-c65tysya.js +1 -0
- package/static/dist/assets/onboarding_model_label-gpn28uix.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-l6jdka8p.js → packet-AYTQ26CC-hatq7m2e.js} +1 -1
- package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
- package/static/dist/assets/{pegDiagram-XKGWAZYB-g1er3bxj.js → pegDiagram-XKGWAZYB-gwisz8jv.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-i0ylcrt9.js → pie-WAS4IAKB-fpftf0mk.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-fbz1e55z.js → pieDiagram-E7YTZNPT-khm1o9a6.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-owqc4uqn.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-f9b70aa4.js → radar-RG4KPBEZ-cntfwjpq.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-lgsuoeda.js → railroad-74A4TZTK-a7t5eibp.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-ol8cdktf.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-i8vzd429.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-nw3ulewz.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-eqpy9fnz.js → railroadDiagram-O6MQD6OU-etuh0qa0.js} +1 -1
- package/static/dist/assets/{refresh-cw-hlwb02qv.js → refresh-cw-mqbkv2a3.js} +1 -1
- package/static/dist/assets/refresh-l29ljp76.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-ogf6puhi.js → requirementDiagram-BXWQKSXE-oq2ulk22.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-iqc02emv.js} +1 -1
- package/static/dist/assets/saved-ndd4t2xb.js +1 -0
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.js → sequenceDiagram-WJ2MYXX4-i0v116q0.js} +2 -2
- package/static/dist/assets/settings_section_extensions_title-jhfo6o8z.js +1 -0
- package/static/dist/assets/share._token-c1ri0hy1.js +2 -0
- package/static/dist/assets/sliders-horizontal-j3jjt9tv.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-fyoznpcs.js → stateDiagram-D77RDMKH-dnpt1ato.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-bxjacpk6.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-oefldhfy.js → swimlanes-42K2YHIH-loqbdazb.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-id62p9hz.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-ol2hnvma.js} +1 -1
- package/static/dist/assets/toast-i3h34v6o.js +64 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-f7hcci5o.js → treeView-Q6P3EWNA-idrj6iox.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-nl85071a.js → treemap-WGGIJYW6-lzr49vv5.js} +1 -1
- package/static/dist/assets/untitled-oaoya4qj.js +1 -0
- package/static/dist/assets/useForm-nfaxgmsk.js +1 -0
- package/static/dist/assets/{useParams-b4dq6n4q.js → useParams-gjc70qru.js} +1 -1
- package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-e6ae539u.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-h5hzbk0n.js → vennDiagram-4TSXK5OY-bvwr9o2z.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-jftvpm3e.js → wardley-WFR3VGLG-31ui8tma.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-hedh85ge.js → wardleyDiagram-VM6X3IG4-k5wqtx4b.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.js → xychartDiagram-S5SC5T6Z-aqbgvujg.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +1 -1
- package/static/dist/assets/AppShell-g3bkgnvt.js +0 -1
- package/static/dist/assets/ChatPage-oo7bdan6.js +0 -25
- package/static/dist/assets/Dialog-g355qxhp.js +0 -1
- package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +0 -1
- package/static/dist/assets/HubRoute-lmkrbw47.js +0 -2
- package/static/dist/assets/Markdown-j4kut4j2.js +0 -598
- package/static/dist/assets/RightPanel-ohlsdzfl.js +0 -1
- package/static/dist/assets/Select-kddq75qc.js +0 -1
- package/static/dist/assets/SessionListPanel-lu3oy0hw.js +0 -1
- package/static/dist/assets/SettingsLayout-o9bv4mgb.js +0 -1
- package/static/dist/assets/States-eq6fe29m.js +0 -1
- package/static/dist/assets/Toaster-pc247u9f.js +0 -1
- package/static/dist/assets/_app-f8n1i0m1.js +0 -1
- package/static/dist/assets/_app.index-bzzv80hk.js +0 -1
- package/static/dist/assets/_app.insights-dzixbryi.js +0 -1
- package/static/dist/assets/_app.kanban-c6hep2us.js +0 -1
- package/static/dist/assets/_app.logs-i69qds98.js +0 -1
- package/static/dist/assets/_app.memory-kl4vf0gd.js +0 -1
- package/static/dist/assets/_app.profiles-b8zinmyp.js +0 -1
- package/static/dist/assets/_app.settings-cg8plwmf.js +0 -1
- package/static/dist/assets/_app.settings._section-bkwbi9zn.js +0 -3
- package/static/dist/assets/_app.skills-cs47sbvs.js +0 -1
- package/static/dist/assets/_app.tasks-dwvk64tc.js +0 -1
- package/static/dist/assets/_app.todos-c8xjm8p3.js +0 -1
- package/static/dist/assets/_app.workspaces-ehvd1gds.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
- package/static/dist/assets/back-h7aun7ew.js +0 -1
- package/static/dist/assets/cancel-fhi0vcnu.js +0 -1
- package/static/dist/assets/channel-b2zo73z1.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +0 -1
- package/static/dist/assets/copied-hcplv46p.js +0 -1
- package/static/dist/assets/endpoints-klbxod4t.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
- package/static/dist/assets/host-i828xuo7.js +0 -1
- package/static/dist/assets/index-eh8zilnv.css +0 -2
- package/static/dist/assets/index-r648f4o1.js +0 -11
- package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
- package/static/dist/assets/link-oot2yn73.js +0 -1
- package/static/dist/assets/login-gu28l9ou.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +0 -1
- package/static/dist/assets/onboarding-lkphzu1s.js +0 -1
- package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +0 -1
- package/static/dist/assets/refresh-abtu93ke.js +0 -1
- package/static/dist/assets/saved-girnabkf.js +0 -1
- package/static/dist/assets/settings_section_extensions_title-h14cwk6f.js +0 -1
- package/static/dist/assets/share._token-kl0r7ido.js +0 -2
- package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
- package/static/dist/assets/sse-iwku0sjo.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +0 -8
- package/static/dist/assets/todoStore-jjr5yr78.js +0 -1
- package/static/dist/assets/units-kgxzxfhi.js +0 -1
- package/static/dist/assets/untitled-cikfjntn.js +0 -1
- package/static/dist/assets/useForm-kqoqg0k9.js +0 -1
- package/static/dist/assets/useLocale-o59tbiea.js +0 -1
package/dist/sessions/turn.js
CHANGED
|
@@ -6,39 +6,43 @@
|
|
|
6
6
|
* `api/streaming.py::_run_agent_streaming`, `cancel_stream`).
|
|
7
7
|
*/
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
|
-
import { closeSync, fstatSync, readFileSync
|
|
9
|
+
import { closeSync, fstatSync, readFileSync } from 'node:fs';
|
|
10
10
|
import { openAnchoredFd } from '../workspace/fs.js';
|
|
11
11
|
import { isWithin, resolvePathLikePython } from '../workspace/paths.js';
|
|
12
|
-
import { SidecarError } from '../sidecar/client.js';
|
|
12
|
+
import { runtimeFailureDetails, SidecarError } from '../sidecar/client.js';
|
|
13
13
|
import { HttpFailure, markSessionTitleGenerated } from './service.js';
|
|
14
14
|
import { StreamRegistry, SessionChannels } from './streams.js';
|
|
15
|
-
import { ChatUsageSchema } from '@maudecode/talaria-web-contracts';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { COMPRESSION_RECOVERY_ACTION_START_FOCUSED, COMPRESSION_RECOVERY_TERMINAL_STATE, CONTEXT_USAGE_FIELDS, Session, contextUsage, titleFrom } from './session.js';
|
|
15
|
+
import { ChatUsageSchema, ConnectorOperationSnapshotSchema } from '@maudecode/talaria-web-contracts';
|
|
16
|
+
import { catalogNames, connectorOperation } from '../tools/connectors.js';
|
|
17
|
+
import { PendingPrompts, clarifyReply, isVaultPrompt, vaultReply } from './pending.js';
|
|
18
|
+
import { COMPRESSION_RECOVERY_ACTION_START_FOCUSED, COMPRESSION_RECOVERY_TERMINAL_STATE, CONTEXT_USAGE_FIELDS, Session, contextUsage, forgetContextFigures, titleFrom } from './session.js';
|
|
19
19
|
import { anchorMessageKey, compressionReference, markerSummary, visibleMessagesForAnchor } from './compress.js';
|
|
20
20
|
import { buildActiveTurnToken, completedToolIndex, publicToolFrame, redactNestedMessageContainers, redactSessionData, redactString, withToolId } from '../redact.js';
|
|
21
|
-
import { dict } from '../config/agent-config.js';
|
|
21
|
+
import { dict, findPersonality } from '../config/agent-config.js';
|
|
22
22
|
import { ReasoningTitleTracker, reasoningEventPayload } from './reasoning-titles.js';
|
|
23
23
|
import { messageWindowForDisplay, messagesForLimitedPayload, toolCallsForMessageWindow } from './window.js';
|
|
24
24
|
import { UNSETTLED_TODO_KEY, attachTodoState, parseTodoToolResult } from './todo.js';
|
|
25
|
-
import {
|
|
25
|
+
import { SessionNotices } from './notices.js';
|
|
26
|
+
import { SessionGoals } from './goals.js';
|
|
26
27
|
import { hydrateAnchorActivityScenes, turnTerminalState, withTurnIds } from './anchor.js';
|
|
27
28
|
import { persistentStateChanges, persistentStateSnapshot } from './state-saved.js';
|
|
28
|
-
import {
|
|
29
|
-
import { agentSteerText, assistantReplyAddedAfterCurrentTurn, attachedFilesPrompt, buildPartialMessage, dedupeContext, checkpointTurnStart, extractToolCallsFromMessages, hasNativeImages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, sessionLacksFinalAssistantAnswer, splitDisplayText, stateDbSeenId, joinReasoning, reasoningFieldsText, toolOutcome, withAttachmentObjects, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withToolCallOutcomes, withoutMaxIterationSummaryRequest, withNativeImagesRestored, withoutNativeImages, withoutToolImages } from './merge.js';
|
|
30
|
-
import { mayBecomeSilentReply, turnOrigin,
|
|
29
|
+
import { agentBuildParams, webuiEphemeralSystemPrompt, workspaceSystemMessage } from './turn-context.js';
|
|
30
|
+
import { agentSteerText, assistantReplyAddedAfterCurrentTurn, attachedFilesPrompt, buildPartialMessage, dedupeContext, checkpointTurnStart, extractToolCallsFromMessages, hasNativeImages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, sessionLacksFinalAssistantAnswer, splitDisplayText, stateDbSeenId, joinReasoning, reasoningFieldsText, toolOutcome, withAttachmentObjects, newestSettledReply, withBodyExcerpts, withDisplayMedia, withMarkerKinds, withToolCallOutcomes, withoutMaxIterationSummaryRequest, withNativeImagesRestored, withoutNativeImages, withoutToolImages } from './merge.js';
|
|
31
|
+
import { mayBecomeSilentReply, turnOrigin, recordBackgroundReview, recordBackgroundUpdate } from './background-updates.js';
|
|
31
32
|
import { countExchanges, fallbackTitleFromExchange, firstExchangeSnippets, isGenericFallbackTitle, latestExchangeSnippets, looksInvalidGeneratedTitle, looksLikeDefaultCliTitle, sanitizeGeneratedTitle, titleLanguageMismatch, titlePrompts } from './titles.js';
|
|
32
33
|
import { WORKSPACE_BUSY_MESSAGE } from '../workspace/git.js';
|
|
33
34
|
import { CHECKPOINT_STORE_BUSY_MESSAGE } from '../workspace/rollback.js';
|
|
34
35
|
import { str } from '../util.js';
|
|
35
36
|
import { toolEditDiff } from './tool-display.js';
|
|
36
37
|
import { recordWakeupPause } from './wakeup-pause.js';
|
|
38
|
+
import { displayName } from '../providers/catalog.js';
|
|
37
39
|
export const CHAT_LOCK_WAIT_SECONDS = 2;
|
|
38
40
|
const IMAGE_MODE_TIMEOUT_MS = 15_000;
|
|
39
41
|
const TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT = 80;
|
|
40
42
|
/** TAL-397: streamed text reports the live meter at most this often (seconds); a counter change reports at once. */
|
|
41
43
|
const METERING_INTERVAL_S = 1;
|
|
44
|
+
/** The Agent's content frames, which end a rate-limit wait (TAL-821) and a compression span (TAL-1006). A terminal frame ends the run, its wait, and its span. */
|
|
45
|
+
const CONTENT_EVENTS = new Set(['token', 'reasoning', 'interim_assistant', 'tool', 'tool_complete']);
|
|
42
46
|
/** How long a user's message waits for a stopped background turn to unwind before its own turn is admitted. */
|
|
43
47
|
const BACKGROUND_UNWIND_WAIT_MS = 30_000;
|
|
44
48
|
const COMPRESSION_EXHAUSTED = { label: 'Context compression exhausted', type: 'compression_exhausted', hint: 'The conversation context is too large to compress safely. Start a new conversation or retry with a narrower task.' };
|
|
@@ -65,6 +69,9 @@ export function classifyProviderError(errStr, opts = {}) {
|
|
|
65
69
|
const lower = errStr.toLowerCase();
|
|
66
70
|
if (opts.compressionExhausted)
|
|
67
71
|
return COMPRESSION_EXHAUSTED;
|
|
72
|
+
// TAL-798: the Agent's local llama.cpp provider has no key to add; its server is off or still starting.
|
|
73
|
+
if (opts.condition === 'local_runtime_unavailable')
|
|
74
|
+
return { label: 'Local model server unavailable', type: 'local_runtime_unavailable', hint: 'The local model server is off or still starting. Turn it on in Hermes Agent, or pick another model.' };
|
|
68
75
|
if (opts.condition === 'credential_missing')
|
|
69
76
|
return { label: 'Authentication failed', type: 'auth_mismatch', hint: 'The selected model may not be supported by your configured provider or your API key is invalid. Run `hermes model` in your terminal to update credentials, then restart the WebUI.' };
|
|
70
77
|
const cancelled = ['cancelled by user', 'canceled by user', 'user cancelled', 'user canceled', 'task cancelled', 'task canceled', 'cancellederror'].some((k) => lower.includes(k));
|
|
@@ -209,6 +216,10 @@ export class TurnRunner {
|
|
|
209
216
|
liveUsage = new Map();
|
|
210
217
|
/** TAL-397: each running turn's latest todo list, which an error or cancel keeps on the session. */
|
|
211
218
|
liveTodos = new Map();
|
|
219
|
+
/** The current presentation, scoped to its owning stream and released on every terminal path. */
|
|
220
|
+
runStatuses = new Map();
|
|
221
|
+
/** TAL-1006: each live stream's open compression span, by its start (epoch seconds). */
|
|
222
|
+
compressionSpans = new Map();
|
|
212
223
|
/** Streams whose run completed (`done` emitted) and only await title work; a late cancel is a no-op for these. */
|
|
213
224
|
settledStreams = new Set();
|
|
214
225
|
/** The last adaptive title refresh per session; a successor turn's refresh runs after it (TAL-589). */
|
|
@@ -222,14 +233,121 @@ export class TurnRunner {
|
|
|
222
233
|
stopContexts = new Map();
|
|
223
234
|
/** Each turn's worker, so a stopped background turn can be awaited before the user's turn takes the session. */
|
|
224
235
|
workers = new Map();
|
|
236
|
+
/** TAL-899: sessions whose user pressed Stop; no automatic turn starts in them until the user's next turn is admitted. */
|
|
237
|
+
stopHeld = new Set();
|
|
238
|
+
notices = new SessionNotices(() => this.deps.now());
|
|
239
|
+
goals = new SessionGoals(() => this.deps.now());
|
|
225
240
|
constructor(deps) {
|
|
226
241
|
this.deps = deps;
|
|
227
242
|
}
|
|
228
243
|
get registry() { return this.deps.registry; }
|
|
229
244
|
// ── admission ────────────────────────────────────────────────────────────
|
|
245
|
+
/** TAL-899: whether a Stop holds the session's automatic turns (session `automatic_turns_held`). */
|
|
246
|
+
async automaticTurnsHeld(sessionId) {
|
|
247
|
+
if (this.stopHeld.has(sessionId))
|
|
248
|
+
return true;
|
|
249
|
+
try {
|
|
250
|
+
return (await this.deps.store.get(sessionId, { metadataOnly: true })).automatic_turns_stop_held;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** TAL-803: the Agent's active notices for the session (session `notices`). */
|
|
257
|
+
sessionNotices(sessionId) {
|
|
258
|
+
const redact = this.deps.redactEnabled();
|
|
259
|
+
return this.notices.list(sessionId).map((n) => ({ ...n, text: redactString(n.text, redact) }));
|
|
260
|
+
}
|
|
261
|
+
/** TAL-808: the session's standing goal (session `goal`). */
|
|
262
|
+
sessionGoal(sessionId) {
|
|
263
|
+
const goal = this.goals.view(sessionId);
|
|
264
|
+
if (!goal)
|
|
265
|
+
return null;
|
|
266
|
+
const redact = this.deps.redactEnabled();
|
|
267
|
+
return { ...goal, text: redactString(goal.text, redact), reason: goal.reason === null ? null : redactString(goal.reason, redact) };
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* TAL-808: reads the session's goal from the Agent and, when its view changed, sends `session_goal` on `put` (else the
|
|
271
|
+
* session's live stream) and `sessions_changed`, which reloads the session for a client between turns.
|
|
272
|
+
*/
|
|
273
|
+
async refreshGoal(s, put = async (event, data) => { (await this.emitToSession(s.session_id, event, data)); }) {
|
|
274
|
+
if (!(await this.readGoal(s)))
|
|
275
|
+
return;
|
|
276
|
+
await put('session_goal', { session_id: s.session_id, goal: this.sessionGoal(s.session_id) });
|
|
277
|
+
this.deps.events.publish('goal', { profile: s.profile, sessionId: s.session_id });
|
|
278
|
+
}
|
|
279
|
+
/** TAL-808: a session's first detail after a restart reads its goal before answering, so it tells no one else. */
|
|
280
|
+
async loadGoal(sessionId) {
|
|
281
|
+
if (this.goals.has(sessionId))
|
|
282
|
+
return;
|
|
283
|
+
let s;
|
|
284
|
+
try {
|
|
285
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
await this.readGoal(s);
|
|
291
|
+
}
|
|
292
|
+
/** The Agent's `goals.get` into the cache, and a re-read when a timed park ends; whether the view changed. A failed read keeps the last one. */
|
|
293
|
+
async readGoal(s) {
|
|
294
|
+
const sidecar = this.deps.sidecar();
|
|
295
|
+
if (!sidecar)
|
|
296
|
+
return false;
|
|
297
|
+
let status;
|
|
298
|
+
try {
|
|
299
|
+
// Short: a session's first load waits on it.
|
|
300
|
+
status = await sidecar.call('goals.get', { session_id: s.session_id, profile_home: this.deps.profileHome(s.profile) }, { timeoutMs: 3_000 });
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
this.deps.log(`[webui] WARNING: goal read failed for ${s.session_id}: ${error.message}`);
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const changed = this.goals.set(s.session_id, status);
|
|
307
|
+
this.goals.rereadAfterPark(s.session_id, () => { void this.refreshGoal({ session_id: s.session_id, profile: s.profile }); });
|
|
308
|
+
return changed;
|
|
309
|
+
}
|
|
310
|
+
/** Hygiene sweep: a deleted session's hold, notices and goal go with it (TAL-899, TAL-803, TAL-808). */
|
|
311
|
+
async sweep() {
|
|
312
|
+
const exists = async (sid) => (await this.deps.store.exists(sid));
|
|
313
|
+
for (const sid of this.stopHeld)
|
|
314
|
+
if (!(await exists(sid)))
|
|
315
|
+
this.stopHeld.delete(sid);
|
|
316
|
+
await this.notices.sweep(exists);
|
|
317
|
+
await this.goals.sweep(exists);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Admit automatic turns through the same workspace recovery and model resolution as user turns. After the user's Stop
|
|
321
|
+
* none starts (TAL-899): the answer is a `held` 409, and the caller keeps its work for the user's next turn.
|
|
322
|
+
*/
|
|
323
|
+
async startServerTurn(session, opts) {
|
|
324
|
+
if ((await this.automaticTurnsHeld(session.session_id)))
|
|
325
|
+
return { _status: 409, error: 'automatic turns are held after Stop', held: true };
|
|
326
|
+
try {
|
|
327
|
+
const service = this.deps.service();
|
|
328
|
+
const workspace = (await service.turnWorkspace(session));
|
|
329
|
+
const [model, modelProvider, normalizedModel] = service.turnModel(session);
|
|
330
|
+
return (await this.admit(session, { ...opts, workspace, model, modelProvider, normalizedModel }));
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
if (error instanceof HttpFailure)
|
|
334
|
+
return { _status: error.status, error: error.message };
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/** A user's turn; admitting it releases the hold a Stop put on automatic turns (TAL-899). */
|
|
339
|
+
async start(session, opts) {
|
|
340
|
+
const response = (await this.admit(session, opts, true));
|
|
341
|
+
if (response._status === undefined)
|
|
342
|
+
this.stopHeld.delete(session.session_id);
|
|
343
|
+
return response;
|
|
344
|
+
}
|
|
230
345
|
/** Python `_start_chat_stream_for_session`: persist pending state, register the channel, launch the worker. */
|
|
231
|
-
|
|
346
|
+
async admit(session, opts, releaseStopHold = false) {
|
|
232
347
|
const s = session;
|
|
348
|
+
const readOnly = (await this.deps.service().readOnlyReason(s));
|
|
349
|
+
if (readOnly)
|
|
350
|
+
return { error: readOnly, _status: 403 };
|
|
233
351
|
if (this.deps.updateInProgress?.())
|
|
234
352
|
return { error: 'Web is updating. Retry after it restarts.', _status: 503 };
|
|
235
353
|
if (this.deps.profileDeleting?.(s.profile ?? null))
|
|
@@ -242,7 +360,7 @@ export class TurnRunner {
|
|
|
242
360
|
if (locked) {
|
|
243
361
|
if (this.registry.liveIds.has(locked) || this.registry.activeRuns.has(locked))
|
|
244
362
|
return { error: 'session already has an active stream', active_stream_id: locked, _status: 409 };
|
|
245
|
-
this.deps.service().clearStaleStreamState(s);
|
|
363
|
+
await this.deps.service().clearStaleStreamState(s);
|
|
246
364
|
if (s.active_stream_id)
|
|
247
365
|
return { error: 'session already has an active stream', active_stream_id: s.active_stream_id, _status: 409 };
|
|
248
366
|
}
|
|
@@ -251,11 +369,21 @@ export class TurnRunner {
|
|
|
251
369
|
if (blocking)
|
|
252
370
|
return { error: 'session already has an active stream', active_stream_id: blocking, _status: 409 };
|
|
253
371
|
}
|
|
372
|
+
const accepted = isDict(s.extra.completion_acceptances) ? s.extra.completion_acceptances : {};
|
|
373
|
+
if (opts.completionKeys?.length && opts.completionKeys.every((key) => typeof accepted[key] === 'string' && accepted[key])) {
|
|
374
|
+
return { stream_id: str(accepted[opts.completionKeys[0]]), session_id: s.session_id };
|
|
375
|
+
}
|
|
254
376
|
const streamId = randomUUID().replace(/-/g, '');
|
|
255
377
|
const wasHiddenEmpty = !s.messages.length && !s.context_messages.length && !s.hasPendingPrompt;
|
|
256
378
|
const attachments = opts.attachments ?? [];
|
|
257
379
|
// A background wakeup stays one in a forked session too (TAL-460).
|
|
258
380
|
const source = opts.source !== 'process_wakeup' && str(s.source_tag).toLowerCase() === 'fork' ? 'fork' : (opts.source ?? 'webui');
|
|
381
|
+
// TAL-1005: the context figures the last turn reported hold for a turn on the same model only, and are judged before
|
|
382
|
+
// admission clears the post-compression estimate. A `/btw` clone is thrown away after one answer, so it is never due.
|
|
383
|
+
const sameRuntime = str(s.model) === str(opts.model) && str(s.model_provider) === str(opts.modelProvider);
|
|
384
|
+
const compactionDue = !opts.ephemeral && sameRuntime && this.deps.service().compactionDue(s);
|
|
385
|
+
if (!sameRuntime)
|
|
386
|
+
forgetContextFigures(s);
|
|
259
387
|
s.workspace = opts.workspace;
|
|
260
388
|
s.model = opts.model;
|
|
261
389
|
s.model_provider = opts.modelProvider;
|
|
@@ -265,6 +393,12 @@ export class TurnRunner {
|
|
|
265
393
|
s.pending_attachments = attachments;
|
|
266
394
|
s.pending_started_at = this.deps.now();
|
|
267
395
|
s.pending_user_source = source;
|
|
396
|
+
// TAL-927: reasoning one provider issued can fail on another (hermes-agent#122831), so a provider switch moves the
|
|
397
|
+
// boundary its carriers replay from.
|
|
398
|
+
if (s.reasoning_replay_provider !== str(opts.modelProvider)) {
|
|
399
|
+
s.reasoning_replay_provider = str(opts.modelProvider);
|
|
400
|
+
s.reasoning_replay_from = s.pending_started_at;
|
|
401
|
+
}
|
|
268
402
|
if (['', 'Untitled', 'New Chat'].includes(str(s.title).trim())) {
|
|
269
403
|
const provisional = titleFrom([{ role: 'user', content: opts.msg }], s.title || 'Untitled');
|
|
270
404
|
if (provisional && !['', 'Untitled', 'New Chat'].includes(provisional.trim()))
|
|
@@ -272,19 +406,39 @@ export class TurnRunner {
|
|
|
272
406
|
}
|
|
273
407
|
if (this.deps.saveMode() === 'eager')
|
|
274
408
|
this.checkpointUserMessage(s, opts.msg, attachments, s.pending_started_at, source, streamId);
|
|
275
|
-
|
|
409
|
+
const previousStopHold = s.automatic_turns_stop_held;
|
|
410
|
+
if (releaseStopHold)
|
|
411
|
+
s.automatic_turns_stop_held = false;
|
|
412
|
+
const previousAcceptances = s.extra.completion_acceptances;
|
|
413
|
+
if (opts.completionKeys?.length)
|
|
414
|
+
s.extra.completion_acceptances = { ...accepted, ...Object.fromEntries(opts.completionKeys.map((key) => [key, streamId])) };
|
|
415
|
+
const previousUpdates = s.extra.background_updates;
|
|
416
|
+
if (opts.backgroundUpdate)
|
|
417
|
+
recordBackgroundUpdate(s, streamId, opts.backgroundUpdate);
|
|
418
|
+
const admission = { stream_id: streamId, session_id: s.session_id, started_at: this.deps.now(), phase: 'starting', workspace: opts.workspace, model: opts.model, provider: opts.modelProvider, ephemeral: Boolean(opts.ephemeral), origin: turnOrigin(source), admitting: true };
|
|
419
|
+
this.registry.registerActiveRun(admission);
|
|
420
|
+
try {
|
|
421
|
+
(await this.deps.store.save(s));
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
this.registry.activeRuns.delete(streamId);
|
|
425
|
+
s.extra.background_updates = previousUpdates;
|
|
426
|
+
s.extra.completion_acceptances = previousAcceptances;
|
|
427
|
+
s.automatic_turns_stop_held = previousStopHold;
|
|
428
|
+
throw error;
|
|
429
|
+
}
|
|
276
430
|
this.registry.writebackOwners.set(s.session_id, streamId);
|
|
277
431
|
// A new chat appears in the list; every turn tells open clients a run started (TAL-434).
|
|
278
432
|
this.deps.events.publish(wasHiddenEmpty ? 'session_new' : 'turn_started', { profile: s.profile, sessionId: s.session_id });
|
|
279
433
|
try {
|
|
280
|
-
this.deps.workspaces.setLastWorkspace(opts.workspace, s.profile);
|
|
434
|
+
(await this.deps.workspaces.setLastWorkspace(opts.workspace, s.profile));
|
|
281
435
|
}
|
|
282
436
|
catch { /* best effort */ }
|
|
283
437
|
const channel = this.registry.create(streamId, s.session_id);
|
|
438
|
+
admission.admitting = false;
|
|
284
439
|
if (opts.goalRelated)
|
|
285
440
|
this.registry.goalRelated.add(streamId);
|
|
286
|
-
this.
|
|
287
|
-
const worker = this.run(s.session_id, streamId, channel, opts).catch((error) => {
|
|
441
|
+
const worker = this.run(s.session_id, streamId, channel, opts, compactionDue).catch((error) => {
|
|
288
442
|
this.deps.log(`[webui] ERROR turn ${streamId} crashed\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
289
443
|
}).finally(() => { this.workers.delete(streamId); });
|
|
290
444
|
this.workers.set(streamId, worker);
|
|
@@ -307,25 +461,69 @@ export class TurnRunner {
|
|
|
307
461
|
s.messages.push(pendingUserRow(msg, attachments, startedAt, source, turnId));
|
|
308
462
|
}
|
|
309
463
|
// ── worker ───────────────────────────────────────────────────────────────
|
|
310
|
-
async run(sessionId, streamId, channel, opts) {
|
|
464
|
+
async run(sessionId, streamId, channel, opts, compactionDue) {
|
|
311
465
|
const { deps } = this;
|
|
312
466
|
let writer = null;
|
|
313
467
|
try {
|
|
314
|
-
writer = deps.journal.writer(sessionId, streamId);
|
|
468
|
+
writer = (await deps.journal.writer(sessionId, streamId));
|
|
315
469
|
this.writers.set(streamId, writer);
|
|
316
470
|
}
|
|
317
471
|
catch (error) {
|
|
318
472
|
deps.log(`[webui] WARNING: run journal degraded for stream ${streamId}: ${error.message}`);
|
|
319
473
|
}
|
|
320
474
|
const failure = { apperror: null, cancelled: false };
|
|
321
|
-
|
|
475
|
+
let waitDetail = null;
|
|
476
|
+
let providerPhase = null;
|
|
477
|
+
let statusEnded = false;
|
|
478
|
+
// TAL-1006: the open compression span closes on the Agent's end of it, else on the next content frame.
|
|
479
|
+
const closeCompression = async (createdAt) => {
|
|
480
|
+
if (!this.compressionSpans.has(streamId))
|
|
481
|
+
return;
|
|
482
|
+
await put('compressed', { session_id: sessionId, message: 'Compression finished' }, createdAt === undefined ? {} : { createdAt });
|
|
483
|
+
};
|
|
484
|
+
const put = async (event, data, meta = {}) => {
|
|
485
|
+
if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn', 'run_status'].includes(event))
|
|
486
|
+
return;
|
|
487
|
+
const terminal = ['done', 'cancel', 'apperror', 'error', 'stream_end'].includes(event);
|
|
488
|
+
if (terminal)
|
|
489
|
+
statusEnded = true;
|
|
490
|
+
if (CONTENT_EVENTS.has(event) || terminal)
|
|
491
|
+
providerPhase = null;
|
|
492
|
+
if (event === 'rate_limit_wait')
|
|
493
|
+
waitDetail = data.wait;
|
|
494
|
+
// Both pre-turn compaction and Agent callbacks publish through this boundary.
|
|
495
|
+
if (event === 'compressing' && !statusEnded) {
|
|
496
|
+
const startedAt = this.compressionSpans.get(streamId) ?? meta.createdAt ?? deps.now();
|
|
497
|
+
this.compressionSpans.set(streamId, startedAt);
|
|
498
|
+
data = { ...data, started_at: startedAt };
|
|
499
|
+
}
|
|
500
|
+
else if (event === 'compressed')
|
|
501
|
+
this.compressionSpans.delete(streamId);
|
|
502
|
+
if (CONTENT_EVENTS.has(event)) {
|
|
503
|
+
if (waitDetail) {
|
|
504
|
+
await put('rate_limit_wait', { session_id: sessionId, wait: null }, meta.createdAt === undefined ? {} : { createdAt: meta.createdAt });
|
|
505
|
+
}
|
|
506
|
+
await closeCompression(meta.createdAt);
|
|
507
|
+
}
|
|
508
|
+
const status = event === 'run_status' ? data.status : statusEnded ? null : this.compressionSpans.has(streamId) ? { phase: 'compressing' }
|
|
509
|
+
: waitDetail ? { phase: 'rate_limit_wait', wait: waitDetail } : providerPhase ?? { phase: 'working' };
|
|
510
|
+
if (JSON.stringify(this.runStatuses.get(streamId) ?? null) !== JSON.stringify(status)) {
|
|
511
|
+
if (status)
|
|
512
|
+
this.runStatuses.set(streamId, status);
|
|
513
|
+
else
|
|
514
|
+
this.runStatuses.delete(streamId);
|
|
515
|
+
await put('run_status', { session_id: sessionId, status }, meta);
|
|
516
|
+
}
|
|
517
|
+
// The sidecar phase is private; only the selected status crosses the client boundary.
|
|
518
|
+
if (event === 'provider_phase')
|
|
519
|
+
return;
|
|
322
520
|
if (event === 'apperror')
|
|
323
521
|
failure.apperror = data;
|
|
324
522
|
else if (event === 'cancel')
|
|
325
523
|
failure.cancelled = true;
|
|
326
524
|
for (const steerEvent of this.takeSteerEventsBefore(streamId, event))
|
|
327
|
-
put(steerEvent[0], steerEvent[1]);
|
|
328
|
-
if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn'].includes(event))
|
|
525
|
+
(await put(steerEvent[0], steerEvent[1]));
|
|
526
|
+
if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn', 'run_status'].includes(event))
|
|
329
527
|
return;
|
|
330
528
|
// `cancel()` already wrote the terminal row and closed the stream: the worker's unwind adds no second one.
|
|
331
529
|
if (event === 'cancel' && !this.registry.streams.has(streamId)) {
|
|
@@ -338,7 +536,7 @@ export class TurnRunner {
|
|
|
338
536
|
let eventId = null;
|
|
339
537
|
if (writer) {
|
|
340
538
|
try {
|
|
341
|
-
eventId = writer.appendSseEvent(event, data, meta).event_id;
|
|
539
|
+
eventId = (await writer.appendSseEvent(event, data, meta)).event_id;
|
|
342
540
|
}
|
|
343
541
|
catch (error) {
|
|
344
542
|
this.registry.degradedJournals.add(streamId);
|
|
@@ -360,25 +558,43 @@ export class TurnRunner {
|
|
|
360
558
|
const activeRun = this.registry.activeRuns.get(streamId);
|
|
361
559
|
let s;
|
|
362
560
|
try {
|
|
363
|
-
s = deps.store.get(sessionId);
|
|
561
|
+
s = (await deps.store.get(sessionId));
|
|
364
562
|
}
|
|
365
563
|
catch {
|
|
366
|
-
put('apperror', { type: 'error', terminal_state: 'error', message: 'Session not found', session_id: sessionId });
|
|
367
|
-
this.teardown(sessionId, streamId);
|
|
564
|
+
await put('apperror', { type: 'error', terminal_state: 'error', message: 'Session not found', session_id: sessionId });
|
|
565
|
+
await this.teardown(sessionId, streamId);
|
|
368
566
|
return;
|
|
369
567
|
}
|
|
370
568
|
const msgText = opts.msg;
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
569
|
+
const history = () => {
|
|
570
|
+
// TAL-493: one state.db read builds the history and marks where this turn's own rows begin.
|
|
571
|
+
const startRead = deps.service().beginStateDbTurn(s);
|
|
572
|
+
const previousMessages = structuredClone(s.messages);
|
|
573
|
+
// TAL-578: an eager save already put this turn's prompt in the transcript; `user_message` carries it, so the history
|
|
574
|
+
// leaves it out.
|
|
575
|
+
const previousContext = structuredClone(deps.service().modelContext(s, startRead.rows)).filter((m) => m._turn_id !== streamId);
|
|
576
|
+
// Python `_sanitize_messages_for_agent`: the model never sees display-only rows or a replayed cancelled prompt, and
|
|
577
|
+
// the Agent gets back the exact bytes it sent (TAL-541).
|
|
578
|
+
const apiHistory = sanitizeMessagesForApi(previousContext, { preserveApiContent: true, issuedReasoningSince: s.reasoning_replay_from });
|
|
579
|
+
this.stopContexts.set(streamId, { previousContext, historyLength: apiHistory.length, prompt: msgText, msgText, checkpointed: false, stateDbStartId: startRead.ok ? stateDbSeenId(startRead.rows) ?? 0 : null, stateDbStartGeneration: startRead.generation });
|
|
580
|
+
return { previousMessages, previousContext, apiHistory };
|
|
581
|
+
};
|
|
380
582
|
// Before the first await: a Stop can land at any point after admission.
|
|
381
|
-
|
|
583
|
+
let { previousMessages, previousContext, apiHistory } = history();
|
|
584
|
+
const controller = new AbortController();
|
|
585
|
+
this.abortControllers.set(streamId, controller);
|
|
586
|
+
// TAL-1005: an over-threshold session compacts in its joinable compression job before the turn reads its history, so
|
|
587
|
+
// a Stop ends only this wait and a resend joins the same job. The turn then reads the compacted session afresh.
|
|
588
|
+
const compacted = await deps.service().ensureCompacted(sessionId, { turn: streamId, signal: controller.signal, due: compactionDue, onWait: async () => { (await put('compressing', { session_id: sessionId, message: 'Compressing context' })); } });
|
|
589
|
+
if (compacted) {
|
|
590
|
+
try {
|
|
591
|
+
s = (await deps.store.get(sessionId));
|
|
592
|
+
}
|
|
593
|
+
catch { /* the cancel or error path below reports a deleted session */ }
|
|
594
|
+
;
|
|
595
|
+
({ previousMessages, previousContext, apiHistory } = history());
|
|
596
|
+
await put('compressed', this.compressedFrame(s));
|
|
597
|
+
}
|
|
382
598
|
const activeTurnToken = buildActiveTurnToken(streamId, s.pending_started_at);
|
|
383
599
|
const sidecar = deps.sidecar();
|
|
384
600
|
const partialText = this.registry.partialText.get(streamId) ?? [];
|
|
@@ -395,21 +611,19 @@ export class TurnRunner {
|
|
|
395
611
|
// TAL-397: the live meter. Counters start from the session's and follow the Agent's; the rate counts text deltas.
|
|
396
612
|
const liveUsage = { input_tokens: s.input_tokens, output_tokens: s.output_tokens, estimated_cost: s.estimated_cost, cache_read_tokens: s.cache_read_tokens, cache_write_tokens: s.cache_write_tokens };
|
|
397
613
|
const meter = { deltas: 0, first: 0, last: 0, sentAt: null };
|
|
398
|
-
const putMetering = (force) => {
|
|
399
|
-
const now = deps.now();
|
|
614
|
+
const putMetering = async (force, now = deps.now()) => {
|
|
400
615
|
if (!force && meter.sentAt !== null && now - meter.sentAt < METERING_INTERVAL_S)
|
|
401
616
|
return;
|
|
402
617
|
meter.sentAt = now;
|
|
403
618
|
const tps = meter.deltas && meter.last > meter.first ? Math.round((meter.deltas / (meter.last - meter.first)) * 10) / 10 : null;
|
|
404
|
-
put('metering', { session_id: sessionId, usage: meteringUsage(liveUsage), tps, tps_available: tps !== null, estimated: false });
|
|
619
|
+
await put('metering', { session_id: sessionId, usage: meteringUsage(liveUsage), tps, tps_available: tps !== null, estimated: false });
|
|
405
620
|
};
|
|
406
|
-
const meterDelta = () => {
|
|
407
|
-
const now = deps.now();
|
|
621
|
+
const meterDelta = async (now = deps.now()) => {
|
|
408
622
|
if (!meter.deltas)
|
|
409
623
|
meter.first = now;
|
|
410
624
|
meter.last = now;
|
|
411
625
|
meter.deltas += 1;
|
|
412
|
-
putMetering(false);
|
|
626
|
+
await putMetering(false, now);
|
|
413
627
|
};
|
|
414
628
|
// TAL-460: a background turn's text is held back while it could still be a silence marker, so one never flashes by.
|
|
415
629
|
// ponytail: checks the turn's whole text, so a marker after earlier prose streams until the settled row hides it;
|
|
@@ -418,13 +632,11 @@ export class TurnRunner {
|
|
|
418
632
|
let heldText = '';
|
|
419
633
|
const titles = new ReasoningTitleTracker();
|
|
420
634
|
let capturedTerminalError = null;
|
|
421
|
-
const controller = new AbortController();
|
|
422
|
-
this.abortControllers.set(streamId, controller);
|
|
423
635
|
const attachments = opts.attachments ?? [];
|
|
424
636
|
// TAL-545: one image mode decides both this turn's upload and the replayed history, which a text-mode turn sends
|
|
425
637
|
// without native images; only a turn with an image on either side asks for it.
|
|
426
638
|
const imageMode = attachments.some(isImageAttachment) || hasNativeImages(apiHistory) ? await this.imageInputMode(sessionId, s, opts, controller.signal) : null;
|
|
427
|
-
const userMessage = this.buildUserMessage(msgText, attachments, opts.workspace, sessionId, imageMode);
|
|
639
|
+
const userMessage = this.buildUserMessage(opts.prompt ?? msgText, attachments, opts.workspace, sessionId, imageMode);
|
|
428
640
|
const conversationHistory = imageMode === 'text' ? withoutNativeImages(apiHistory) : apiHistory;
|
|
429
641
|
// The prompt the Agent actually gets, native image parts included.
|
|
430
642
|
const stop = this.stopContexts.get(streamId);
|
|
@@ -434,6 +646,10 @@ export class TurnRunner {
|
|
|
434
646
|
activeRun.phase = 'running';
|
|
435
647
|
const settledAt = { value: false };
|
|
436
648
|
let succeeded = false;
|
|
649
|
+
let ownershipRefused = false;
|
|
650
|
+
let agentDispatched = false;
|
|
651
|
+
let streamWrites = Promise.resolve();
|
|
652
|
+
let streamWriteError;
|
|
437
653
|
// TAL-424: steers an errored turn ended without taking, sent as one follow-up turn once this one is torn down.
|
|
438
654
|
let followUp = [];
|
|
439
655
|
try {
|
|
@@ -443,6 +659,11 @@ export class TurnRunner {
|
|
|
443
659
|
// system message carries the frozen session workspace, the WebUI guidance rides as the ephemeral prompt with this
|
|
444
660
|
// turn's workspace (TAL-710: never a tag on the user's message, which the Agent can persist from memory).
|
|
445
661
|
const cfg = (await deps.profileConfig?.(s.profile ?? null)) ?? {};
|
|
662
|
+
const personality = str(s.personality);
|
|
663
|
+
// A selected personality never silently drops: an unknown catalog fails the turn (an absent name is the Agent's no-overlay).
|
|
664
|
+
const personalities = personality && deps.personalities
|
|
665
|
+
? await deps.personalities(s.profile ?? null).catch((error) => { throw new SidecarError(`The session's personality "${personality}" could not be resolved: ${error instanceof Error ? error.message : String(error)}`, { condition: 'sidecar_error' }); })
|
|
666
|
+
: [];
|
|
446
667
|
// The sidecar's per-session YOLO state does not survive its restarts: push the local flag (either way) before
|
|
447
668
|
// the turn so a stale sidecar-side enable cannot auto-approve a session the UI reports as guarded.
|
|
448
669
|
await this.withYoloLock(sessionId, async () => { try {
|
|
@@ -452,153 +673,226 @@ export class TurnRunner {
|
|
|
452
673
|
// Last yield before `chat.start`: a cancel that landed during the attachment, config or YOLO awaits must not
|
|
453
674
|
// start an Agent turn (the client also refuses an already-aborted signal outright).
|
|
454
675
|
if (this.registry.cancelled.has(streamId)) {
|
|
455
|
-
this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
456
|
-
put('cancel', this.cancelFrame(sessionId));
|
|
676
|
+
await this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
677
|
+
await put('cancel', (await this.cancelFrame(sessionId)));
|
|
457
678
|
return;
|
|
458
679
|
}
|
|
459
680
|
const turnContext = {
|
|
460
681
|
system_message: workspaceSystemMessage(str(s.created_workspace) || str(s.workspace)),
|
|
461
|
-
ephemeral_system_prompt: webuiEphemeralSystemPrompt({ config: cfg,
|
|
462
|
-
|
|
463
|
-
max_tokens: maxTokensFromConfig(cfg),
|
|
464
|
-
reasoning_config: reasoningConfigFromConfig(cfg, opts.modelProvider ?? s.model_provider ?? null),
|
|
682
|
+
ephemeral_system_prompt: webuiEphemeralSystemPrompt({ config: cfg, personalityPrompt: findPersonality(personalities, personality)?.prompt ?? null, sessionId, profile: s.profile ?? null, workspace: opts.workspace, hermesHome: deps.profileHome(s.profile), homeDisplay: deps.homeDisplay?.() ?? '~/.hermes', background: turnOrigin(opts.source) === 'background' }),
|
|
683
|
+
...agentBuildParams(cfg, opts.source ?? 'webui', opts.modelProvider ?? s.model_provider ?? null, deps.env ?? {}),
|
|
465
684
|
};
|
|
466
685
|
// Python `_public_prefill_context_status`: the session-recall prefill hook was dropped with TAL-245, so the
|
|
467
686
|
// context frame always reports the not-configured shape.
|
|
468
687
|
const persistentBefore = persistentStateSnapshot(deps.profileHome(s.profile));
|
|
469
|
-
put('context_status', { session_id: sessionId, prefill: { status: 'not_configured', source: 'none', label: '', message_count: 0 } });
|
|
688
|
+
await put('context_status', { session_id: sessionId, prefill: { status: 'not_configured', source: 'none', label: '', message_count: 0 } });
|
|
689
|
+
// Setup awaits can outlive a gateway adoption; no await separates this read from dispatch.
|
|
690
|
+
deps.service().assertWritable(s);
|
|
691
|
+
agentDispatched = true;
|
|
470
692
|
const result = await sidecar.call('chat.start', {
|
|
471
|
-
profile_home: deps.profileHome(s.profile), session_id: sessionId, stream_id: streamId,
|
|
693
|
+
profile_home: deps.profileHome(s.profile), session_id: sessionId, stream_id: streamId, ...this.agentWorkspace(s, opts.workspace), model: opts.model ?? '', model_provider: opts.modelProvider,
|
|
472
694
|
user_message: userMessage, ...turnContext, conversation_history: conversationHistory, enabled_toolsets: deps.toolsetsFor(s), ...(msgText ? { persist_user_message: msgText } : {}), ...(opts.ephemeral ? { ephemeral: true } : {}),
|
|
695
|
+
...(opts.voiceTurn ? { voice_turn: true } : {}),
|
|
696
|
+
...(opts.connectCard ? { connect_card: true } : {}),
|
|
697
|
+
...(opts.skillSecret ? { skill_secret: true } : {}),
|
|
473
698
|
}, {
|
|
474
699
|
signal: controller.signal,
|
|
475
700
|
timeoutMs: 0,
|
|
476
701
|
onStream: (frame) => {
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
702
|
+
const receivedAt = deps.now();
|
|
703
|
+
streamWrites = streamWrites.then(async () => {
|
|
704
|
+
const publish = put;
|
|
705
|
+
const emitFrame = (event, data, meta = {}) => publish(event, data, { ...meta, createdAt: receivedAt });
|
|
706
|
+
const data = isDict(frame.data) ? frame.data : {};
|
|
707
|
+
switch (frame.event) {
|
|
708
|
+
case 'token': {
|
|
709
|
+
tokenSent = true;
|
|
710
|
+
firstTokenAt ??= receivedAt;
|
|
711
|
+
// Visible text closes the reasoning segment: a stable title snapshot (or a clear) goes out first.
|
|
712
|
+
const stable = titles.stableSnapshot(reasoningText.join(''));
|
|
713
|
+
if (stable !== null)
|
|
714
|
+
(await emitFrame('reasoning', { text: '', titles: stable }));
|
|
715
|
+
partialText.push(str(data.text));
|
|
716
|
+
if (holdSilence && mayBecomeSilentReply(partialText.join(''))) {
|
|
717
|
+
heldText += str(data.text);
|
|
718
|
+
(await meterDelta(receivedAt));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
await emitFrame('token', { text: heldText + str(data.text) });
|
|
722
|
+
heldText = '';
|
|
723
|
+
await meterDelta(receivedAt);
|
|
490
724
|
return;
|
|
491
725
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
reasoningText.push(str(data.text));
|
|
501
|
-
// Python `reasoning_event_payload`: bold or first-line titles ride on the delta they complete.
|
|
502
|
-
put('reasoning', titles.prepare(reasoningEventPayload(str(data.text), reasoningText.join(''))));
|
|
503
|
-
meterDelta();
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
case 'usage': {
|
|
507
|
-
const usage = ChatUsageSchema.safeParse(data);
|
|
508
|
-
if (usage.success) {
|
|
509
|
-
applyAgentUsage(liveUsage, usage.data);
|
|
510
|
-
this.liveUsage.set(streamId, liveUsage);
|
|
726
|
+
case 'reasoning': {
|
|
727
|
+
if (partialText.length && !reasoningText.length)
|
|
728
|
+
titles.resetSegment();
|
|
729
|
+
reasoningText.push(str(data.text));
|
|
730
|
+
// Python `reasoning_event_payload`: bold or first-line titles ride on the delta they complete.
|
|
731
|
+
await emitFrame('reasoning', titles.prepare(reasoningEventPayload(str(data.text), reasoningText.join(''))));
|
|
732
|
+
await meterDelta(receivedAt);
|
|
733
|
+
return;
|
|
511
734
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
735
|
+
case 'usage': {
|
|
736
|
+
const usage = ChatUsageSchema.safeParse(data);
|
|
737
|
+
if (usage.success) {
|
|
738
|
+
applyAgentUsage(liveUsage, usage.data);
|
|
739
|
+
this.liveUsage.set(streamId, liveUsage);
|
|
740
|
+
}
|
|
741
|
+
await putMetering(true, receivedAt);
|
|
519
742
|
return;
|
|
520
743
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
case 'tool_complete': {
|
|
533
|
-
// TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
|
|
534
|
-
// are its display sections, which keep the stderr and exit code the flat preview drops.
|
|
535
|
-
const { raw_result: rawResult, todo_result: todoResult, result_diff: resultDiff, ...complete } = data;
|
|
536
|
-
const outcome = toolOutcome(rawResult);
|
|
537
|
-
complete.is_error = outcome.is_error;
|
|
538
|
-
if (rawResult !== undefined)
|
|
539
|
-
complete.result_view = outcome.result_view;
|
|
540
|
-
// TAL-448: a file edit's change, counted over the whole diff the sidecar forwards; redacted with the frame.
|
|
541
|
-
const editDiff = toolEditDiff(data.name, { diff: resultDiff });
|
|
542
|
-
if (editDiff)
|
|
543
|
-
complete.edit_diff = editDiff;
|
|
544
|
-
const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
|
|
545
|
-
if (tc) {
|
|
546
|
-
const startedAt = toolStartedAt.get(tc);
|
|
547
|
-
if (startedAt !== undefined)
|
|
548
|
-
complete.duration = Math.round(Math.max(0, deps.now() - startedAt) * 1000) / 1000;
|
|
549
|
-
// TAL-315: the decided view too, so a failed or cancelled turn's snapshot keeps the sections shown live.
|
|
550
|
-
Object.assign(tc, { done: true, snippet: data.preview, is_error: complete.is_error, duration: complete.duration ?? null, ...(complete.result_view ? { result_view: complete.result_view } : {}), ...(editDiff ? { edit_diff: editDiff } : {}) });
|
|
744
|
+
case 'steer_pending': {
|
|
745
|
+
// TAL-424: while a withdraw or Send now rewrites the Agent's queue its pending text is ambiguous; it settles after.
|
|
746
|
+
if (this.steerRewrites.has(streamId)) {
|
|
747
|
+
this.deferredPendingSteer.set(streamId, str(data.text));
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
// Capture the causal position before the next frame; only persistence is asynchronous.
|
|
751
|
+
const saving = this.settleConsumedSteers(sessionId, streamId, str(data.text));
|
|
752
|
+
await saving;
|
|
753
|
+
return;
|
|
551
754
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
this.liveTodos.set(streamId, todos);
|
|
561
|
-
put('todo_state', redactNestedMessageContainers({ ...todos, session_id: sessionId, stream_id: streamId, source: 'live' }, redacted));
|
|
755
|
+
case 'tool': {
|
|
756
|
+
const call = { name: data.name, args: data.args ?? {}, tid: str(data.tid), done: false };
|
|
757
|
+
liveToolCalls.push(call);
|
|
758
|
+
toolIds.set(call, call.tid || mintToolId());
|
|
759
|
+
toolStartedAt.set(call, receivedAt);
|
|
760
|
+
const redacted = deps.redactEnabled();
|
|
761
|
+
await emitFrame('tool', publicToolFrame(withToolId(data, toolIds.get(call)), redacted), { redacted });
|
|
762
|
+
return;
|
|
562
763
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
764
|
+
case 'tool_complete': {
|
|
765
|
+
// TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
|
|
766
|
+
// are its display sections, which keep the stderr and exit code the flat preview drops.
|
|
767
|
+
const { raw_result: rawResult, todo_result: todoResult, result_diff: resultDiff, display_metadata: displayMetadata, ...complete } = data;
|
|
768
|
+
const outcome = toolOutcome(rawResult);
|
|
769
|
+
complete.is_error = outcome.is_error;
|
|
770
|
+
if (rawResult !== undefined)
|
|
771
|
+
complete.result_view = outcome.result_view;
|
|
772
|
+
// TAL-448: a file edit's change, counted over the whole diff the sidecar forwards; redacted with the frame. TAL-909:
|
|
773
|
+
// else the diff in the metadata the Agent persisted on the result row, which a reload reads the same way.
|
|
774
|
+
const editDiff = toolEditDiff(data.name, { diff: resultDiff }, displayMetadata);
|
|
775
|
+
if (editDiff)
|
|
776
|
+
complete.edit_diff = editDiff;
|
|
777
|
+
const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
|
|
778
|
+
if (tc) {
|
|
779
|
+
const startedAt = toolStartedAt.get(tc);
|
|
780
|
+
if (startedAt !== undefined)
|
|
781
|
+
complete.duration = Math.round(Math.max(0, receivedAt - startedAt) * 1000) / 1000;
|
|
782
|
+
// TAL-315: the decided view too, so a failed or cancelled turn's snapshot keeps the sections shown live.
|
|
783
|
+
Object.assign(tc, { done: true, snippet: data.preview, is_error: complete.is_error, duration: complete.duration ?? null, ...(complete.result_view ? { result_view: complete.result_view } : {}), ...(editDiff ? { edit_diff: editDiff } : {}) });
|
|
784
|
+
}
|
|
785
|
+
const id = (tc && toolIds.get(tc)) || str(data.tid) || mintToolId();
|
|
786
|
+
this.lastCompletedTool.set(streamId, id);
|
|
787
|
+
const redacted = deps.redactEnabled();
|
|
788
|
+
await emitFrame('tool_complete', publicToolFrame(withToolId(complete, id), redacted), { redacted });
|
|
789
|
+
// TAL-397: the todo tool's result is the session's new list; the settled session derives the same one.
|
|
790
|
+
// TAL-889: the sidecar attaches `todo_result` only to the Agent's todo tool, so no name check here.
|
|
791
|
+
const todos = parseTodoToolResult(todoResult);
|
|
792
|
+
if (todos) {
|
|
793
|
+
todos.ts = receivedAt;
|
|
794
|
+
this.liveTodos.set(streamId, todos);
|
|
795
|
+
await emitFrame('todo_state', redactNestedMessageContainers({ ...todos, session_id: sessionId, stream_id: streamId, source: 'live' }, redacted));
|
|
796
|
+
}
|
|
575
797
|
return;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
798
|
+
}
|
|
799
|
+
// Python: the live chat frame carries the queue head plus depth, not the entry that just arrived.
|
|
800
|
+
case 'approval': {
|
|
801
|
+
deps.pending.submitApproval(sessionId, { ...data, session_id: sessionId });
|
|
802
|
+
const { pending, pending_count } = deps.pending.approvalPending(sessionId);
|
|
803
|
+
await emitFrame('approval', { ...(pending ?? data), pending_count });
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
// TAL-514: the Agent stopped waiting on an approval (answer, timeout, or interrupt); withdraw its copy.
|
|
807
|
+
case 'approval_resolved': {
|
|
808
|
+
if (!str(data.approval_id))
|
|
809
|
+
return;
|
|
810
|
+
deps.pending.resolveApproval(sessionId, str(data.approval_id));
|
|
811
|
+
await emitFrame(...this.approvalHeadFrame(sessionId));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
case 'clarify': {
|
|
815
|
+
deps.pending.submitClarify(sessionId, { ...data, session_id: sessionId });
|
|
816
|
+
const head = deps.pending.clarifyHeadFrame(sessionId);
|
|
817
|
+
if (head)
|
|
818
|
+
(await emitFrame('clarify', head));
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
case 'clarify_resolved': {
|
|
822
|
+
const { head } = deps.pending.resolveClarify(sessionId, str(data.clarify_id));
|
|
823
|
+
if (head)
|
|
824
|
+
(await emitFrame('clarify', head));
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
case 'provider_phase': {
|
|
828
|
+
const phase = data.phase;
|
|
829
|
+
if (phase !== null && phase?.phase !== 'prompt_reading')
|
|
830
|
+
return;
|
|
831
|
+
const percent = phase?.progress_percent;
|
|
832
|
+
providerPhase = phase === null ? null : { phase: 'prompt_reading', progress_percent: typeof percent === 'number' && Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : null };
|
|
833
|
+
await put('provider_phase', {}, { createdAt: receivedAt });
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
// TAL-821: the Agent sits out a provider's rate limit (`until`, epoch seconds: its reset, else the Agent's retry).
|
|
837
|
+
case 'rate_limit_wait': {
|
|
838
|
+
const until = typeof data.until === 'number' ? data.until : null;
|
|
839
|
+
if (until === null && !waitDetail)
|
|
840
|
+
return;
|
|
841
|
+
const provider = str(data.provider) || null;
|
|
842
|
+
await emitFrame('rate_limit_wait', { session_id: sessionId, wait: until === null ? null : { provider, provider_label: provider && (deps.providerLabel?.(s.profile ?? null, provider) ?? displayName(provider)), until, until_kind: data.until_kind === 'reset' ? 'reset' : 'retry' } });
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
// TAL-818: a `manage_connections` card opened; clients poll and answer the operation by its `op_id`.
|
|
846
|
+
case 'connection': {
|
|
847
|
+
const snapshot = ConnectorOperationSnapshotSchema.safeParse(data);
|
|
848
|
+
if (snapshot.success)
|
|
849
|
+
(await emitFrame('connect_card', connectorOperation(snapshot.data, catalogNames(deps.profileHome(s.profile)))));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
case 'compressing':
|
|
853
|
+
await emitFrame('compressing', data);
|
|
854
|
+
return;
|
|
855
|
+
case 'compression_finished':
|
|
856
|
+
await closeCompression(receivedAt);
|
|
857
|
+
return;
|
|
858
|
+
case 'status':
|
|
859
|
+
if (data.kind === 'terminal_error' && !capturedTerminalError)
|
|
860
|
+
capturedTerminalError = str(data.message);
|
|
861
|
+
return;
|
|
862
|
+
// TAL-803: the session's notice list changed; every client replaces its copy.
|
|
863
|
+
case 'notice': {
|
|
864
|
+
const notice = this.notices.add(sessionId, data);
|
|
865
|
+
if (!notice)
|
|
866
|
+
return;
|
|
867
|
+
await emitFrame('notices', { session_id: sessionId, notices: this.sessionNotices(sessionId) });
|
|
868
|
+
// ponytail: the expiry reaches a client only while a turn streams; an idle tab drops it on its next session load.
|
|
869
|
+
if (notice.expires_at !== null) {
|
|
870
|
+
const expiresAt = notice.expires_at;
|
|
871
|
+
const emitAfterExpiry = async () => {
|
|
872
|
+
// Timers can run before the wall clock expires the notice. Let SessionNotices decide, then retry.
|
|
873
|
+
if (this.notices.list(sessionId).includes(notice))
|
|
874
|
+
setTimeout(() => { void emitAfterExpiry().catch((error) => { deps.log(`[webui] notice expiry failed: ${String(error)}`); }); }, Math.max(1, Math.ceil((expiresAt - deps.now()) * 1000))).unref();
|
|
875
|
+
else
|
|
876
|
+
(await this.emitToSession(sessionId, 'notices', { session_id: sessionId, notices: this.sessionNotices(sessionId) }));
|
|
877
|
+
};
|
|
878
|
+
await emitAfterExpiry();
|
|
879
|
+
}
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
case 'notice_clear':
|
|
883
|
+
case 'notices_reset':
|
|
884
|
+
if (frame.event === 'notice_clear' ? this.notices.clear(sessionId, str(data.key)) : this.notices.reset(sessionId))
|
|
885
|
+
(await emitFrame('notices', { session_id: sessionId, notices: this.sessionNotices(sessionId) }));
|
|
886
|
+
return;
|
|
887
|
+
default:
|
|
888
|
+
await emitFrame(frame.event, data);
|
|
592
889
|
}
|
|
593
|
-
|
|
594
|
-
if (data.kind === 'terminal_error' && !capturedTerminalError)
|
|
595
|
-
capturedTerminalError = str(data.message);
|
|
596
|
-
return;
|
|
597
|
-
default:
|
|
598
|
-
put(frame.event, data);
|
|
599
|
-
}
|
|
890
|
+
}).catch((error) => { streamWriteError = error instanceof Error ? error : new Error(String(error)); });
|
|
600
891
|
},
|
|
601
892
|
});
|
|
893
|
+
await streamWrites;
|
|
894
|
+
if (streamWriteError !== undefined)
|
|
895
|
+
throw streamWriteError;
|
|
602
896
|
settledAt.value = true;
|
|
603
897
|
// TAL-545: every transcript the Agent hands back settles with the history's images, not the request's projection.
|
|
604
898
|
const withImages = (rows) => withNativeImagesRestored(rows, conversationHistory, apiHistory);
|
|
@@ -611,14 +905,14 @@ export class TurnRunner {
|
|
|
611
905
|
const interrupted = await this.interrupts.get(streamId);
|
|
612
906
|
applyAgentUsage(liveUsage, result.usage);
|
|
613
907
|
this.liveUsage.set(streamId, liveUsage);
|
|
614
|
-
this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ? withImages(interrupted.checkpoint) : result.messages);
|
|
615
|
-
put('cancel', this.cancelFrame(sessionId));
|
|
908
|
+
await this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ? withImages(interrupted.checkpoint) : result.messages);
|
|
909
|
+
await put('cancel', (await this.cancelFrame(sessionId)));
|
|
616
910
|
return;
|
|
617
911
|
}
|
|
618
912
|
// Writeback only while this stream still owns the session (Python `_stream_writeback_is_current`).
|
|
619
913
|
let current;
|
|
620
914
|
try {
|
|
621
|
-
current = deps.store.get(sessionId);
|
|
915
|
+
current = (await deps.store.get(sessionId));
|
|
622
916
|
}
|
|
623
917
|
catch {
|
|
624
918
|
current = s;
|
|
@@ -654,15 +948,15 @@ export class TurnRunner {
|
|
|
654
948
|
await this.steerRewrites.get(streamId);
|
|
655
949
|
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, str(result.pending_steer), 'followup');
|
|
656
950
|
followUp = leftovers;
|
|
657
|
-
this.persistError(s, streamId, classification.label, payload, activeTurnToken, resultMessages, result.provider);
|
|
951
|
+
await this.persistError(s, streamId, classification.label, payload, activeTurnToken, resultMessages, result.provider);
|
|
658
952
|
// TAL-512: a btw error carries no session: its journaled frame must not keep a copy of the parent conversation.
|
|
659
953
|
if (!opts.ephemeral)
|
|
660
|
-
payload.session = this.publicTerminalSession(s);
|
|
954
|
+
payload.session = (await this.publicTerminalSession(s));
|
|
661
955
|
payload.session_id = s.session_id;
|
|
662
956
|
payload.old_session_id = sessionId;
|
|
663
957
|
for (const [event, data] of steerEvents)
|
|
664
|
-
put(event, data);
|
|
665
|
-
put('apperror', payload);
|
|
958
|
+
(await put(event, data));
|
|
959
|
+
await put('apperror', payload);
|
|
666
960
|
return;
|
|
667
961
|
}
|
|
668
962
|
// TAL-512: a btw turn passes the failure check above first, so its last assistant row is this turn's reply.
|
|
@@ -676,11 +970,11 @@ export class TurnRunner {
|
|
|
676
970
|
}
|
|
677
971
|
}
|
|
678
972
|
succeeded = true;
|
|
679
|
-
opts.onDone?.(answer);
|
|
973
|
+
await opts.onDone?.(answer);
|
|
680
974
|
// Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
|
|
681
|
-
put('done', { session: { session_id: sessionId, messages: resultMessages.map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
|
|
975
|
+
await put('done', { session: { session_id: sessionId, messages: resultMessages.map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
|
|
682
976
|
try {
|
|
683
|
-
|
|
977
|
+
await deps.store.delete(sessionId, { tombstone: false });
|
|
684
978
|
}
|
|
685
979
|
catch { /* ignore */ }
|
|
686
980
|
deps.store.sessions.delete(sessionId);
|
|
@@ -707,8 +1001,9 @@ export class TurnRunner {
|
|
|
707
1001
|
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, result.pending_steer, 'followup');
|
|
708
1002
|
s.messages = withoutToolImages(mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] }));
|
|
709
1003
|
s.context_messages = dedupeContext(context);
|
|
1004
|
+
// The settled turn's own figures: a compaction it waited on (TAL-1005) installed an estimate its prompt now supersedes.
|
|
1005
|
+
s.post_compression_context_tokens_estimate = result.compressed ? estimate : null;
|
|
710
1006
|
if (result.compressed) {
|
|
711
|
-
s.post_compression_context_tokens_estimate = estimate;
|
|
712
1007
|
// TAL-540: the anchor is the last visible row before this turn (never its eager checkpoint); the merged transcript
|
|
713
1008
|
// holds no compaction marker, so the summary is the model context's newest one.
|
|
714
1009
|
const before = visibleMessagesForAnchor(previousMessages.filter((m) => m._turn_id !== streamId));
|
|
@@ -733,20 +1028,11 @@ export class TurnRunner {
|
|
|
733
1028
|
s.threshold_tokens = result.context.threshold_tokens;
|
|
734
1029
|
if (typeof result.context.last_prompt_tokens === 'number')
|
|
735
1030
|
s.last_prompt_tokens = result.context.last_prompt_tokens;
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
...meteringUsage(s), context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
740
|
-
post_compression_context_tokens_estimate: s.post_compression_context_tokens_estimate, ...contextUsage(s, () => deps.service().deps.contextLengthFor(s.model, s.model_provider)),
|
|
741
|
-
};
|
|
742
|
-
put('compressed', { session_id: sessionId, old_session_id: sessionId, new_session_id: sessionId, continuation_session_id: sessionId, message: 'Compression finished', usage });
|
|
743
|
-
}
|
|
1031
|
+
s.next_turn_threshold_tokens = result.context.next_turn_threshold_tokens ?? null;
|
|
1032
|
+
if (result.compressed)
|
|
1033
|
+
(await put('compressed', this.compressedFrame(s)));
|
|
744
1034
|
s.tool_calls = extractToolCallsFromMessages(s.messages, liveToolCalls, s.tool_calls);
|
|
745
|
-
s
|
|
746
|
-
s.pending_user_message = null;
|
|
747
|
-
s.pending_attachments = [];
|
|
748
|
-
s.pending_started_at = null;
|
|
749
|
-
s.pending_user_source = null;
|
|
1035
|
+
this.clearPendingTurn(s);
|
|
750
1036
|
// TAL-576: a turn that ran means the provider has usable credentials again.
|
|
751
1037
|
s.process_wakeup_pause = null;
|
|
752
1038
|
const attachments = opts.attachments ?? [];
|
|
@@ -801,8 +1087,9 @@ export class TurnRunner {
|
|
|
801
1087
|
this.persistConsumedSteers(s, streamId, previousStartedAt(s, activeRun), now);
|
|
802
1088
|
// TAL-493: the rows this turn's Agent wrote to state.db are now in the transcript, so the merge must not replay them
|
|
803
1089
|
// (all of them, the dropped summary request too).
|
|
804
|
-
|
|
805
|
-
|
|
1090
|
+
this.settleStateDb(s, streamId, agentRows, { previousContext });
|
|
1091
|
+
recordBackgroundReview(s, streamId);
|
|
1092
|
+
await deps.store.save(s);
|
|
806
1093
|
deps.pending.clearApprovals(sessionId);
|
|
807
1094
|
deps.pending.clearClarifies(sessionId);
|
|
808
1095
|
deps.events.publish('session_done', { profile: s.profile, sessionId: s.session_id });
|
|
@@ -810,9 +1097,9 @@ export class TurnRunner {
|
|
|
810
1097
|
try {
|
|
811
1098
|
const changes = persistentStateChanges(persistentBefore, persistentStateSnapshot(deps.profileHome(s.profile)));
|
|
812
1099
|
if (changes.memory_saved)
|
|
813
|
-
put('state_saved', { session_id: sessionId, kind: 'memory', action: 'saved' });
|
|
1100
|
+
(await put('state_saved', { session_id: sessionId, kind: 'memory', action: 'saved' }));
|
|
814
1101
|
for (const change of changes.skills)
|
|
815
|
-
put('state_saved', { session_id: sessionId, kind: 'skill', action: change.action, name: change.name });
|
|
1102
|
+
(await put('state_saved', { session_id: sessionId, kind: 'skill', action: change.action, name: change.name }));
|
|
816
1103
|
}
|
|
817
1104
|
catch (error) {
|
|
818
1105
|
deps.log(`[webui] persistent state change detection failed for ${sessionId}: ${error.message}`);
|
|
@@ -828,7 +1115,7 @@ export class TurnRunner {
|
|
|
828
1115
|
cache_hit_percent: cacheHit(cacheReadTokens, inputTokens), turn_cache_hit_percent: cacheHit(turnCacheRead, turnInput), duration_seconds: Math.round(duration * 1000) / 1000,
|
|
829
1116
|
context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
830
1117
|
};
|
|
831
|
-
const doneSession = this.publicTerminalSession(s);
|
|
1118
|
+
const doneSession = (await this.publicTerminalSession(s));
|
|
832
1119
|
// TAL-299: the ring's figures match the terminal session's (and a detail reload's).
|
|
833
1120
|
for (const key of CONTEXT_USAGE_FIELDS)
|
|
834
1121
|
doneUsage[key] = doneSession[key];
|
|
@@ -847,58 +1134,76 @@ export class TurnRunner {
|
|
|
847
1134
|
if (result.tool_limit_reached)
|
|
848
1135
|
donePayload.terminal_reason = 'max_iterations';
|
|
849
1136
|
// TAL-397: the live meter ends on the persisted counters and turn rate.
|
|
850
|
-
put('metering', { session_id: sessionId, usage: meteringUsage(s), tps: doneUsage.tps ?? null, tps_available: doneUsage.tps !== undefined, estimated: false });
|
|
851
|
-
put('done', donePayload);
|
|
1137
|
+
await put('metering', { session_id: sessionId, usage: meteringUsage(s), tps: doneUsage.tps ?? null, tps_available: doneUsage.tps !== undefined, estimated: false });
|
|
1138
|
+
await put('done', donePayload);
|
|
852
1139
|
for (const [event, payload] of steerEvents)
|
|
853
|
-
put(event, payload);
|
|
1140
|
+
(await put(event, payload));
|
|
854
1141
|
// The turn is over: release admission before the title work so a follow-up message is accepted while the
|
|
855
1142
|
// (up to two) title prompts run; the channel and journal stay open for the `title` events (Python retired the
|
|
856
1143
|
// worker before its daemon-thread title generation).
|
|
857
1144
|
this.registry.activeRuns.delete(streamId);
|
|
858
1145
|
this.settledStreams.add(streamId);
|
|
859
|
-
|
|
1146
|
+
// TAL-1005: a turn that settled at or above the next turn's threshold starts the compaction now, so the next message
|
|
1147
|
+
// usually finds the session compacted; a follow-up turn admitted meanwhile joins it.
|
|
1148
|
+
void deps.service().ensureCompacted(sessionId);
|
|
1149
|
+
await this.startSteerFollowUp(sessionId, leftovers);
|
|
860
1150
|
// Before the title work, so a generated title's own state.db sync lands after this one's provisional title.
|
|
861
1151
|
await deps.syncUsage?.(s, usedModel || null);
|
|
862
1152
|
// Title work and the goal judge run side by side; `stream_end` follows both, so their frames reach the stream.
|
|
863
1153
|
const work = await Promise.allSettled([this.backgroundTitle(s, put), this.continueGoal(s, streamId, put)]);
|
|
864
|
-
put('stream_end', { session_id: sessionId });
|
|
1154
|
+
await put('stream_end', { session_id: sessionId });
|
|
865
1155
|
for (const r of work)
|
|
866
1156
|
if (r.status === 'rejected')
|
|
867
1157
|
throw r.reason;
|
|
868
1158
|
succeeded = true;
|
|
869
|
-
opts.onDone?.(lastAnswer(s.messages));
|
|
1159
|
+
await opts.onDone?.(lastAnswer(s.messages));
|
|
870
1160
|
}
|
|
871
1161
|
catch (error) {
|
|
1162
|
+
await streamWrites;
|
|
1163
|
+
const readOnly = !agentDispatched ? (await deps.service().readOnlyReason(s)) : null;
|
|
1164
|
+
if (readOnly) {
|
|
1165
|
+
ownershipRefused = true;
|
|
1166
|
+
if (this.registry.writebackOwners.get(sessionId) === streamId && s.active_stream_id === streamId) {
|
|
1167
|
+
// Cancellation persists the prompt and a Stop row; an unstarted, refused turn must add neither.
|
|
1168
|
+
s.messages = s.messages.filter((m) => !(m.role === 'user' && m._turn_id === streamId));
|
|
1169
|
+
this.clearPendingTurn(s);
|
|
1170
|
+
if (isDict(s.extra.completion_acceptances))
|
|
1171
|
+
s.extra.completion_acceptances = Object.fromEntries(Object.entries(s.extra.completion_acceptances).filter(([, value]) => value !== streamId));
|
|
1172
|
+
await deps.store.save(s, { touchUpdatedAt: false });
|
|
1173
|
+
}
|
|
1174
|
+
await put('apperror', { type: 'error', terminal_state: 'error', message: readOnly, session_id: sessionId, session: (await this.publicTerminalSession(s)) });
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
872
1177
|
if (settledAt.value && !(error instanceof SidecarError)) {
|
|
873
1178
|
deps.log(`[webui] ERROR settling turn ${streamId}\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
874
1179
|
}
|
|
875
1180
|
if (this.registry.cancelled.has(streamId)) {
|
|
876
|
-
this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
877
|
-
put('cancel', this.cancelFrame(sessionId));
|
|
1181
|
+
await this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
1182
|
+
await put('cancel', (await this.cancelFrame(sessionId)));
|
|
878
1183
|
return;
|
|
879
1184
|
}
|
|
880
1185
|
const message = error instanceof Error ? error.message : String(error);
|
|
881
1186
|
const condition = error instanceof SidecarError ? error.condition : undefined;
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1187
|
+
const runtime = error instanceof SidecarError && ['sidecar_unavailable', 'agent_runtime_stale', 'agent_incompatible'].includes(error.condition)
|
|
1188
|
+
? runtimeFailureDetails(error.condition, error.data) : null;
|
|
1189
|
+
const classification = runtime ? { label: 'Error', type: runtime.type, hint: '' } : classifyProviderError(message, { condition });
|
|
1190
|
+
const payload = providerErrorPayload(runtime?.message ?? message, classification.type, classification.hint, deps.redactEnabled());
|
|
1191
|
+
if (runtime)
|
|
1192
|
+
Object.assign(payload, runtime);
|
|
888
1193
|
try {
|
|
889
1194
|
let current = s;
|
|
890
1195
|
try {
|
|
891
|
-
current = deps.store.get(sessionId);
|
|
1196
|
+
current = (await deps.store.get(sessionId));
|
|
892
1197
|
}
|
|
893
1198
|
catch {
|
|
894
1199
|
current = s;
|
|
895
1200
|
}
|
|
896
1201
|
if (current.active_stream_id === streamId) {
|
|
897
1202
|
for (const [event, data] of this.takeSteerEventsBefore(streamId, 'apperror'))
|
|
898
|
-
put(event, data);
|
|
899
|
-
this.persistError(current, streamId, classification.label, payload, activeTurnToken);
|
|
1203
|
+
(await put(event, data));
|
|
1204
|
+
await this.persistError(current, streamId, classification.label, payload, activeTurnToken);
|
|
900
1205
|
if (!opts.ephemeral)
|
|
901
|
-
payload.session = this.publicTerminalSession(current);
|
|
1206
|
+
payload.session = (await this.publicTerminalSession(current));
|
|
902
1207
|
}
|
|
903
1208
|
}
|
|
904
1209
|
catch (persistError) {
|
|
@@ -906,15 +1211,20 @@ export class TurnRunner {
|
|
|
906
1211
|
}
|
|
907
1212
|
payload.session_id = sessionId;
|
|
908
1213
|
payload.old_session_id = sessionId;
|
|
909
|
-
put('apperror', payload);
|
|
1214
|
+
await put('apperror', payload);
|
|
910
1215
|
}
|
|
911
1216
|
finally {
|
|
912
|
-
this.teardown(sessionId, streamId);
|
|
1217
|
+
await this.teardown(sessionId, streamId);
|
|
913
1218
|
// After persistence and teardown: a background route's failure cleanup deletes the hidden session, which must
|
|
914
1219
|
// not race the error writeback above (a re-saved session would resurface in the sidebar).
|
|
915
1220
|
if (!succeeded)
|
|
916
|
-
opts.onFailed?.(failure);
|
|
917
|
-
|
|
1221
|
+
await opts.onFailed?.(failure);
|
|
1222
|
+
// TAL-1005: a Stop or an error after the turn ran on a compaction's result left that job's estimate under a context
|
|
1223
|
+
// its prompt and work may have grown; the idle trigger judges the context measured again.
|
|
1224
|
+
if (!succeeded && !ownershipRefused && !opts.ephemeral)
|
|
1225
|
+
void deps.service().remeasureContext(sessionId).then((measured) => { if (measured)
|
|
1226
|
+
void deps.service().ensureCompacted(sessionId); });
|
|
1227
|
+
await this.startSteerFollowUp(sessionId, followUp);
|
|
918
1228
|
}
|
|
919
1229
|
}
|
|
920
1230
|
/**
|
|
@@ -995,21 +1305,22 @@ export class TurnRunner {
|
|
|
995
1305
|
return images ? [{ type: 'text', text: withFiles() }, ...parts] : withFiles();
|
|
996
1306
|
}
|
|
997
1307
|
/** The settled session as a terminal frame carries it: the display window, redacted. */
|
|
998
|
-
publicTerminalSession(s) {
|
|
999
|
-
return redactSessionData(this.terminalSessionPayload(s), this.deps.redactEnabled());
|
|
1308
|
+
async publicTerminalSession(s) {
|
|
1309
|
+
return redactSessionData((await this.terminalSessionPayload(s)), this.deps.redactEnabled());
|
|
1000
1310
|
}
|
|
1001
1311
|
/**
|
|
1002
1312
|
* Python `_session_payload_with_terminal_window`: the same visible-message window contract as `GET /api/session?
|
|
1003
1313
|
* msg_limit=` (renderable rows, limited payload shape, windowed tool calls, todo state) with the full count.
|
|
1004
1314
|
*/
|
|
1005
|
-
terminalSessionPayload(s) {
|
|
1006
|
-
const payload =
|
|
1007
|
-
payload.
|
|
1008
|
-
payload.
|
|
1315
|
+
async terminalSessionPayload(s) {
|
|
1316
|
+
const payload = (await this.deps.service().wireRow(s));
|
|
1317
|
+
payload.automatic_turns_held = (await this.automaticTurnsHeld(s.session_id));
|
|
1318
|
+
payload.notices = this.sessionNotices(s.session_id);
|
|
1319
|
+
payload.goal = this.sessionGoal(s.session_id);
|
|
1009
1320
|
const service = this.deps.service();
|
|
1010
|
-
const scened = service.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes(
|
|
1321
|
+
const scened = (await service.backgroundLinked(s, hydrateAnchorActivityScenes(withToolCallOutcomes((await service.backgroundUpdates(s, withMarkerKinds(withTurnIds(withAttachmentObjects(s.messages)), s))), s.tool_calls, s.active_stream_id), s.anchor_activity_scenes, { activeTurnId: s.active_stream_id, clipToolResults: true })));
|
|
1011
1322
|
const [window, offset] = messageWindowForDisplay(scened, TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT, null);
|
|
1012
|
-
const limited = messagesForLimitedPayload(withBodyExcerpts(withDisplayMedia(window, service.mediaProjector(s)), s.active_stream_id));
|
|
1323
|
+
const limited = messagesForLimitedPayload(withBodyExcerpts(withDisplayMedia(window, (await service.mediaProjector(s))), s.active_stream_id, newestSettledReply(scened, s.active_stream_id), offset));
|
|
1013
1324
|
payload.messages = limited;
|
|
1014
1325
|
payload.message_count = s.messages.length;
|
|
1015
1326
|
payload._messages_offset = offset;
|
|
@@ -1019,9 +1330,26 @@ export class TurnRunner {
|
|
|
1019
1330
|
payload.compression_reference = compressionReference(s, scened);
|
|
1020
1331
|
return payload;
|
|
1021
1332
|
}
|
|
1333
|
+
/** TAL-540, Python `_live_usage_snapshot`: a finished compression's frame, with the counters and the ring's post-compression figures, so clients update before `done`. */
|
|
1334
|
+
compressedFrame(s) {
|
|
1335
|
+
const usage = {
|
|
1336
|
+
...meteringUsage(s), context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
1337
|
+
post_compression_context_tokens_estimate: s.post_compression_context_tokens_estimate, ...contextUsage(s, () => this.deps.service().deps.contextLengthFor(s.model, s.model_provider)),
|
|
1338
|
+
};
|
|
1339
|
+
const sid = s.session_id;
|
|
1340
|
+
return { session_id: sid, old_session_id: sid, new_session_id: sid, continuation_session_id: sid, message: 'Compression finished', usage };
|
|
1341
|
+
}
|
|
1342
|
+
/** Release only the pending admission metadata; transcript settlement belongs to the caller. */
|
|
1343
|
+
clearPendingTurn(s) {
|
|
1344
|
+
s.active_stream_id = null;
|
|
1345
|
+
s.pending_user_message = null;
|
|
1346
|
+
s.pending_attachments = [];
|
|
1347
|
+
s.pending_started_at = null;
|
|
1348
|
+
s.pending_user_source = null;
|
|
1349
|
+
}
|
|
1022
1350
|
/** Python `_materialize_pending_user_turn_before_error` + error message append + save. */
|
|
1023
1351
|
/** `runtimeProvider`: the provider the Agent resolved for the turn, when it reported one. */
|
|
1024
|
-
persistError(s, streamId, label, payload, activeTurnToken, agentRows = [], runtimeProvider = '') {
|
|
1352
|
+
async persistError(s, streamId, label, payload, activeTurnToken, agentRows = [], runtimeProvider = '') {
|
|
1025
1353
|
const startedAt = s.pending_started_at;
|
|
1026
1354
|
// TAL-576: an automatic wakeup with no usable credentials pauses the next ones instead of failing each.
|
|
1027
1355
|
if (s.pending_user_source === 'process_wakeup' && payload.type === 'credential_pool_empty') {
|
|
@@ -1038,11 +1366,7 @@ export class TurnRunner {
|
|
|
1038
1366
|
}
|
|
1039
1367
|
this.materializePendingUserTurn(s, activeTurnToken, streamId);
|
|
1040
1368
|
const duration = typeof startedAt === 'number' && startedAt > 0 ? Math.max(0, this.deps.now() - startedAt) : null;
|
|
1041
|
-
s
|
|
1042
|
-
s.pending_user_message = null;
|
|
1043
|
-
s.pending_attachments = [];
|
|
1044
|
-
s.pending_started_at = null;
|
|
1045
|
-
s.pending_user_source = null;
|
|
1369
|
+
this.clearPendingTurn(s);
|
|
1046
1370
|
this.appendPartialSnapshot(s, streamId);
|
|
1047
1371
|
const hint = str(payload.hint);
|
|
1048
1372
|
const errorMessage = { role: 'assistant', content: `**${label}:** ${str(payload.message) || label}${hint ? `\n\n*${hint}*` : ''}`, timestamp: Math.trunc(this.deps.now()), _error: true, _turn_id: streamId };
|
|
@@ -1072,10 +1396,9 @@ export class TurnRunner {
|
|
|
1072
1396
|
const context = payload.type === 'compression_exhausted' ? null : this.unsettledContext(streamId, agentRows, read);
|
|
1073
1397
|
if (context)
|
|
1074
1398
|
s.context_messages = context;
|
|
1075
|
-
|
|
1076
|
-
this.deps.service().settleStateDb(s, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows, startId: stop?.stateDbStartId ?? null, read });
|
|
1399
|
+
this.settleStateDb(s, streamId, agentRows, { read });
|
|
1077
1400
|
try {
|
|
1078
|
-
this.deps.store.save(s);
|
|
1401
|
+
(await this.deps.store.save(s));
|
|
1079
1402
|
}
|
|
1080
1403
|
catch (error) {
|
|
1081
1404
|
this.deps.log(`[webui] WARNING: failed to save error turn for ${s.session_id}: ${error.message}`);
|
|
@@ -1086,6 +1409,12 @@ export class TurnRunner {
|
|
|
1086
1409
|
this.deps.pending.clearClarifies(s.session_id);
|
|
1087
1410
|
this.deps.events.publish('session_error', { profile: s.profile, sessionId: s.session_id });
|
|
1088
1411
|
}
|
|
1412
|
+
/** Every terminal path carries the same starting read's id and generation into settlement. */
|
|
1413
|
+
settleStateDb(s, streamId, agentRows, opts = {}) {
|
|
1414
|
+
const stop = this.stopContexts.get(streamId);
|
|
1415
|
+
const service = this.deps.service();
|
|
1416
|
+
service.settleStateDb(s, { turnId: streamId, previousContext: stop?.previousContext ?? opts.previousContext ?? [], agentRows, startId: stop?.stateDbStartId ?? null, startGeneration: stop?.stateDbStartGeneration ?? null, read: opts.read ?? service.stateDbRead(s) });
|
|
1417
|
+
}
|
|
1089
1418
|
/**
|
|
1090
1419
|
* TAL-921: the model context a turn that ends without its settled result leaves: the work its Agent reported, else the
|
|
1091
1420
|
* rows it committed past its starting read in `read`, the read its settlement uses.
|
|
@@ -1094,7 +1423,7 @@ export class TurnRunner {
|
|
|
1094
1423
|
const stop = this.stopContexts.get(streamId);
|
|
1095
1424
|
if (!stop)
|
|
1096
1425
|
return null;
|
|
1097
|
-
const after = stop.stateDbStartId;
|
|
1426
|
+
const after = stop.stateDbStartGeneration !== null && read.generation !== null && stop.stateDbStartGeneration !== read.generation ? -1 : stop.stateDbStartId;
|
|
1098
1427
|
const stateRows = after === null ? [] : read.rows.filter((m) => typeof m._state_db_row_id === 'number' && m._state_db_row_id > after);
|
|
1099
1428
|
return this.deps.service().unsettledTurnContext({ ...stop, streamedText: (this.registry.partialText.get(streamId) ?? []).join(''), agentRows, stateRows });
|
|
1100
1429
|
}
|
|
@@ -1143,11 +1472,11 @@ export class TurnRunner {
|
|
|
1143
1472
|
* Python `_emit_cancel_event`: the terminal frame carries the settled session so tabs can render the partial without a
|
|
1144
1473
|
* reload. TAL-364: no copy — clients show their one localized status for the `cancelled` outcome.
|
|
1145
1474
|
*/
|
|
1146
|
-
cancelFrame(sessionId) {
|
|
1475
|
+
async cancelFrame(sessionId) {
|
|
1147
1476
|
let snapshot = null;
|
|
1148
1477
|
if (sessionId)
|
|
1149
1478
|
try {
|
|
1150
|
-
snapshot = this.publicTerminalSession(this.deps.store.get(sessionId));
|
|
1479
|
+
snapshot = (await this.publicTerminalSession((await this.deps.store.get(sessionId))));
|
|
1151
1480
|
}
|
|
1152
1481
|
catch {
|
|
1153
1482
|
snapshot = null;
|
|
@@ -1159,12 +1488,12 @@ export class TurnRunner {
|
|
|
1159
1488
|
* turn's model context keeps what the Agent had captured (`checkpoint`, its canonical transcript) and the prose that
|
|
1160
1489
|
* streamed, so the next request continues from it; the terminal row carries the outcome and no copy.
|
|
1161
1490
|
*/
|
|
1162
|
-
finalizeCancelled(s, streamId, ephemeral, checkpoint = null) {
|
|
1491
|
+
async finalizeCancelled(s, streamId, ephemeral, checkpoint = null) {
|
|
1163
1492
|
if (this.registry.writebackOwners.get(s.session_id) !== streamId)
|
|
1164
1493
|
return false;
|
|
1165
1494
|
let current = s;
|
|
1166
1495
|
try {
|
|
1167
|
-
current = this.deps.store.get(s.session_id);
|
|
1496
|
+
current = (await this.deps.store.get(s.session_id));
|
|
1168
1497
|
}
|
|
1169
1498
|
catch {
|
|
1170
1499
|
return false;
|
|
@@ -1173,7 +1502,7 @@ export class TurnRunner {
|
|
|
1173
1502
|
return false;
|
|
1174
1503
|
if (ephemeral) {
|
|
1175
1504
|
try {
|
|
1176
|
-
|
|
1505
|
+
await this.deps.store.delete(current.session_id, { tombstone: false });
|
|
1177
1506
|
}
|
|
1178
1507
|
catch { /* ignore */ }
|
|
1179
1508
|
this.deps.store.sessions.delete(current.session_id);
|
|
@@ -1185,9 +1514,9 @@ export class TurnRunner {
|
|
|
1185
1514
|
if (quiet && current.active_stream_id === null) {
|
|
1186
1515
|
// TAL-493: rows its worker committed while unwinding are covered by what it reports; others are kept.
|
|
1187
1516
|
if (checkpoint) {
|
|
1188
|
-
this.
|
|
1517
|
+
this.settleStateDb(current, streamId, checkpoint);
|
|
1189
1518
|
try {
|
|
1190
|
-
this.deps.store.save(current);
|
|
1519
|
+
(await this.deps.store.save(current));
|
|
1191
1520
|
}
|
|
1192
1521
|
catch {
|
|
1193
1522
|
return false;
|
|
@@ -1216,9 +1545,9 @@ export class TurnRunner {
|
|
|
1216
1545
|
const late = canonical && !stop.checkpointed ? settle(read) : null;
|
|
1217
1546
|
if (late)
|
|
1218
1547
|
current.context_messages = dedupeContext(late);
|
|
1219
|
-
this.
|
|
1548
|
+
this.settleStateDb(current, streamId, checkpoint, { read });
|
|
1220
1549
|
try {
|
|
1221
|
-
this.deps.store.save(current);
|
|
1550
|
+
(await this.deps.store.save(current));
|
|
1222
1551
|
}
|
|
1223
1552
|
catch {
|
|
1224
1553
|
return false;
|
|
@@ -1232,20 +1561,16 @@ export class TurnRunner {
|
|
|
1232
1561
|
const context = settle(read);
|
|
1233
1562
|
if (context)
|
|
1234
1563
|
current.context_messages = dedupeContext(context);
|
|
1235
|
-
current
|
|
1236
|
-
current.pending_user_message = null;
|
|
1237
|
-
current.pending_attachments = [];
|
|
1238
|
-
current.pending_started_at = null;
|
|
1239
|
-
current.pending_user_source = null;
|
|
1564
|
+
this.clearPendingTurn(current);
|
|
1240
1565
|
if (!quiet) {
|
|
1241
1566
|
this.appendPartialSnapshot(current, streamId);
|
|
1242
1567
|
current.messages.push({ role: 'assistant', content: '', _error: true, _terminal_state: 'cancelled', timestamp: Math.trunc(this.deps.now()), _turn_id: streamId });
|
|
1243
1568
|
}
|
|
1244
1569
|
this.keepLiveState(current, streamId);
|
|
1245
1570
|
this.persistConsumedSteers(current, streamId, startedAt, this.deps.now());
|
|
1246
|
-
this.
|
|
1571
|
+
this.settleStateDb(current, streamId, checkpoint, { read });
|
|
1247
1572
|
try {
|
|
1248
|
-
this.deps.store.save(current);
|
|
1573
|
+
(await this.deps.store.save(current));
|
|
1249
1574
|
}
|
|
1250
1575
|
catch {
|
|
1251
1576
|
return false;
|
|
@@ -1257,8 +1582,8 @@ export class TurnRunner {
|
|
|
1257
1582
|
return true;
|
|
1258
1583
|
}
|
|
1259
1584
|
/** Push a frame onto the session's live chat stream from outside the turn (Python gateway notify callbacks). */
|
|
1260
|
-
emitToSession(sessionId, event, data) {
|
|
1261
|
-
this.sessionPuts.get(sessionId)?.(event, data);
|
|
1585
|
+
async emitToSession(sessionId, event, data) {
|
|
1586
|
+
await this.sessionPuts.get(sessionId)?.(event, data);
|
|
1262
1587
|
}
|
|
1263
1588
|
/** TAL-397: a turn that ends without its result keeps what the live stream already showed: counters and todo list. */
|
|
1264
1589
|
keepLiveState(s, streamId) {
|
|
@@ -1269,13 +1594,15 @@ export class TurnRunner {
|
|
|
1269
1594
|
if (todos)
|
|
1270
1595
|
s.extra[UNSETTLED_TODO_KEY] = todos;
|
|
1271
1596
|
}
|
|
1272
|
-
teardown(sessionId, streamId) {
|
|
1597
|
+
async teardown(sessionId, streamId) {
|
|
1598
|
+
this.runStatuses.delete(streamId);
|
|
1273
1599
|
this.liveUsage.delete(streamId);
|
|
1600
|
+
this.compressionSpans.delete(streamId);
|
|
1274
1601
|
this.liveTodos.delete(streamId);
|
|
1275
1602
|
const writer = this.writers.get(streamId);
|
|
1276
1603
|
if (writer) {
|
|
1277
1604
|
try {
|
|
1278
|
-
writer.close();
|
|
1605
|
+
(await writer.close());
|
|
1279
1606
|
}
|
|
1280
1607
|
catch { /* ignore */ }
|
|
1281
1608
|
this.writers.delete(streamId);
|
|
@@ -1304,21 +1631,27 @@ export class TurnRunner {
|
|
|
1304
1631
|
catch { /* best effort */ }
|
|
1305
1632
|
}
|
|
1306
1633
|
/** Journal files with a live writer (retention must leave them alone). */
|
|
1307
|
-
activeJournalPaths() {
|
|
1308
|
-
return new Set([...this.writers.values()].map((w) => this.deps.journal.pathFor(w.sessionId, w.runId)));
|
|
1309
|
-
}
|
|
1310
1634
|
// ── title ────────────────────────────────────────────────────────────────
|
|
1311
1635
|
/** Python `_generate_llm_session_title_via_aux`: aux-route statuses (`llm_aux`, `llm_aux_retry`, `llm_*_aux`). */
|
|
1312
1636
|
async llmTitle(s, userText, assistantText) {
|
|
1313
1637
|
const sidecar = this.deps.sidecar();
|
|
1314
1638
|
if (!sidecar)
|
|
1315
1639
|
return { title: '', status: 'llm_error_aux', rawPreview: '' };
|
|
1640
|
+
const service = this.deps.service();
|
|
1641
|
+
await service.deps.warmSessionModelRepair?.(s.profile);
|
|
1642
|
+
const [model, provider] = service.turnModel(s);
|
|
1316
1643
|
const [qa, prompts] = titlePrompts(userText, assistantText);
|
|
1317
1644
|
let status = 'llm_error_aux';
|
|
1318
1645
|
for (const [idx, prompt] of prompts.entries()) {
|
|
1646
|
+
if ((await this.deps.service().isReadOnly(s)))
|
|
1647
|
+
return { title: '', status: 'read_only', rawPreview: '' };
|
|
1319
1648
|
let raw = '';
|
|
1320
1649
|
try {
|
|
1321
|
-
const result = await sidecar.call('aux.complete', {
|
|
1650
|
+
const result = await sidecar.call('aux.complete', {
|
|
1651
|
+
profile_home: this.deps.profileHome(s.profile), task: 'title_generation',
|
|
1652
|
+
messages: [{ role: 'system', content: prompt }, { role: 'user', content: qa }], max_tokens: 64,
|
|
1653
|
+
main_runtime: { model: model ?? '', provider }, main_fallback: true,
|
|
1654
|
+
});
|
|
1322
1655
|
raw = str(result.text);
|
|
1323
1656
|
}
|
|
1324
1657
|
catch (error) {
|
|
@@ -1339,6 +1672,42 @@ export class TurnRunner {
|
|
|
1339
1672
|
}
|
|
1340
1673
|
return { title: '', status, rawPreview: '' };
|
|
1341
1674
|
}
|
|
1675
|
+
/**
|
|
1676
|
+
* TAL-902: `/refine [focus]` starts the Agent's memory/skill review of the chat's model context on its agent
|
|
1677
|
+
* (`chat.refine`, the agent a turn would build); no turn starts. Without a chat, while a turn runs, or before anything
|
|
1678
|
+
* was said it answers with the Agent's wording instead.
|
|
1679
|
+
*/
|
|
1680
|
+
async refine(s, focus) {
|
|
1681
|
+
if (!s)
|
|
1682
|
+
return 'Nothing to refine yet — send a message first.';
|
|
1683
|
+
if (s.active_stream_id)
|
|
1684
|
+
return 'Agent is running — wait for the turn to finish, then /refine.';
|
|
1685
|
+
const messages = sanitizeMessagesForApi(this.deps.service().modelContext(s), { preserveApiContent: true, issuedReasoningSince: s.reasoning_replay_from });
|
|
1686
|
+
if (!messages.length)
|
|
1687
|
+
return 'Nothing to refine yet — the conversation is empty.';
|
|
1688
|
+
// Like a turn: no agent is built in a profile whose home is going away.
|
|
1689
|
+
if (this.deps.profileDeleting?.(s.profile ?? null))
|
|
1690
|
+
throw new HttpFailure(409, `Profile '${str(s.profile)}' is being deleted.`);
|
|
1691
|
+
const sidecar = this.deps.sidecar();
|
|
1692
|
+
if (!sidecar)
|
|
1693
|
+
throw new SidecarError('The Agent sidecar is not running; /refine is unavailable until it starts.', { condition: 'sidecar_unavailable' });
|
|
1694
|
+
const [model, provider] = this.deps.service().turnModel(s);
|
|
1695
|
+
const cfg = (await this.deps.profileConfig?.(s.profile ?? null)) ?? {};
|
|
1696
|
+
this.deps.service().assertWritable(s);
|
|
1697
|
+
return (await sidecar.call('chat.refine', {
|
|
1698
|
+
profile_home: this.deps.profileHome(s.profile), session_id: s.session_id, messages, focus, model: model ?? '', model_provider: provider,
|
|
1699
|
+
enabled_toolsets: this.deps.toolsetsFor(s), ...agentBuildParams(cfg, 'webui', provider, this.deps.env ?? {}),
|
|
1700
|
+
...this.agentWorkspace(s, (await this.deps.service().turnWorkspace(s))),
|
|
1701
|
+
}, { timeoutMs: 120_000 })).output;
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* TAL-903: the workspace a session's agent is built at, and whether it is a host path (the profile's terminal runs
|
|
1705
|
+
* locally). Only then does the Agent anchor AGENTS.md discovery and its memory provider there; `chat.start` and
|
|
1706
|
+
* `chat.refine` send the same pair, so the agent `/refine` builds is the one the next turn reuses.
|
|
1707
|
+
*/
|
|
1708
|
+
agentWorkspace(s, workspace) {
|
|
1709
|
+
return { workspace, workspace_local: this.deps.workspaces.profileSupportsLocalIo(s.profile) };
|
|
1710
|
+
}
|
|
1342
1711
|
/**
|
|
1343
1712
|
* Python `generate_session_title_for_session`: on-demand title from the persisted transcript; never touches
|
|
1344
1713
|
* `llm_title_generated`. An explicit regenerate runs even with `auxiliary.title_generation.enabled` off (TAL-531).
|
|
@@ -1352,6 +1721,7 @@ export class TurnRunner {
|
|
|
1352
1721
|
let rawPreview = '';
|
|
1353
1722
|
if (assistantText)
|
|
1354
1723
|
({ title: next, status: llmStatus, rawPreview } = await this.llmTitle(s, userText, assistantText));
|
|
1724
|
+
this.deps.service().assertWritable(s);
|
|
1355
1725
|
if (next)
|
|
1356
1726
|
return { title: next, status: llmStatus, rawPreview };
|
|
1357
1727
|
const fallback = fallbackTitleFromExchange(userText, assistantText);
|
|
@@ -1366,7 +1736,7 @@ export class TurnRunner {
|
|
|
1366
1736
|
async titleClaimed(s) {
|
|
1367
1737
|
if (!looksLikeDefaultCliTitle({ title: s.title, source_tag: s.source_tag, raw_source: s.raw_source, session_source: s.session_source, source_label: s.source_label }))
|
|
1368
1738
|
return;
|
|
1369
|
-
await this.backgroundTitle(s, () =>
|
|
1739
|
+
await this.backgroundTitle(s, () => Promise.resolve()).catch((error) => { this.deps.log(`[webui] imported session title failed for ${s.session_id}: ${error.message}`); });
|
|
1370
1740
|
}
|
|
1371
1741
|
/** Python `_run_background_title_update` (spawned only when `_background_title_generation_inputs` is eligible). */
|
|
1372
1742
|
async backgroundTitle(s, put) {
|
|
@@ -1378,8 +1748,8 @@ export class TurnRunner {
|
|
|
1378
1748
|
const eligible = ['Untitled', 'New Chat', ''].includes(placeholder) || placeholder === titleFrom(s.messages, '') || invalidExisting || defaultTitle(s);
|
|
1379
1749
|
const [userText, assistantText] = firstExchangeSnippets(s.messages);
|
|
1380
1750
|
// Python `_put_title_status`: empty reason/title/raw_preview keys are omitted.
|
|
1381
|
-
const status = (status, reason = '', title = '', rawPreview = '') => {
|
|
1382
|
-
put('title_status', { session_id: sessionId, status, ...(reason ? { reason } : {}), ...(title ? { title } : {}), ...(rawPreview ? { raw_preview: rawPreview } : {}) });
|
|
1751
|
+
const status = async (status, reason = '', title = '', rawPreview = '') => {
|
|
1752
|
+
await put('title_status', { session_id: sessionId, status, ...(reason ? { reason } : {}), ...(title ? { title } : {}), ...(rawPreview ? { raw_preview: rawPreview } : {}) });
|
|
1383
1753
|
};
|
|
1384
1754
|
// Python ran the adaptive refresh only when the first-title job was not eligible.
|
|
1385
1755
|
if (!eligible || (s.llm_title_generated && !invalidExisting) || !userText || !assistantText) {
|
|
@@ -1387,21 +1757,25 @@ export class TurnRunner {
|
|
|
1387
1757
|
return;
|
|
1388
1758
|
}
|
|
1389
1759
|
if (s.manual_title) {
|
|
1390
|
-
status('skipped', 'manual_title', placeholder);
|
|
1760
|
+
(await status('skipped', 'manual_title', placeholder));
|
|
1391
1761
|
return;
|
|
1392
1762
|
}
|
|
1393
1763
|
// A missing config.yaml reads as `{}` (enabled); an unreadable one cannot confirm the user has not opted out.
|
|
1394
1764
|
const cfg = await this.deps.profileConfig?.(s.profile ?? null);
|
|
1395
1765
|
if (!cfg) {
|
|
1396
|
-
status('skipped', 'config_unavailable', placeholder);
|
|
1766
|
+
(await status('skipped', 'config_unavailable', placeholder));
|
|
1397
1767
|
return;
|
|
1398
1768
|
}
|
|
1399
1769
|
if (!titleGenerationEnabled(cfg)) {
|
|
1400
|
-
status('skipped', 'title_generation_disabled', placeholder);
|
|
1770
|
+
(await status('skipped', 'title_generation_disabled', placeholder));
|
|
1401
1771
|
return;
|
|
1402
1772
|
}
|
|
1403
1773
|
const generated = await this.llmTitle(s, userText, assistantText);
|
|
1404
1774
|
const { status: llmStatus, rawPreview } = generated;
|
|
1775
|
+
if (llmStatus === 'read_only') {
|
|
1776
|
+
(await status('skipped', 'read_only', placeholder));
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1405
1779
|
let next = generated.title;
|
|
1406
1780
|
let source = llmStatus;
|
|
1407
1781
|
if (!next) {
|
|
@@ -1414,26 +1788,30 @@ export class TurnRunner {
|
|
|
1414
1788
|
const fallbackReason = source === 'fallback' && llmStatus ? `local_summary:${llmStatus}` : 'local_summary';
|
|
1415
1789
|
let current = s;
|
|
1416
1790
|
try {
|
|
1417
|
-
current = this.deps.store.get(sessionId);
|
|
1791
|
+
current = (await this.deps.store.get(sessionId));
|
|
1418
1792
|
}
|
|
1419
1793
|
catch {
|
|
1420
1794
|
// A session deleted while the title prompt ran stays deleted.
|
|
1421
|
-
if (this.deps.store.wasDeleted(sessionId))
|
|
1795
|
+
if ((await this.deps.store.wasDeleted(sessionId)))
|
|
1422
1796
|
return;
|
|
1423
1797
|
current = s;
|
|
1424
1798
|
}
|
|
1799
|
+
if ((await this.deps.service().isReadOnly(current))) {
|
|
1800
|
+
(await status('skipped', 'read_only', str(current.title).trim()));
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1425
1803
|
let effective = str(current.title).trim();
|
|
1426
1804
|
let wrote = false;
|
|
1427
1805
|
if (next) {
|
|
1428
1806
|
const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title) || defaultTitle(current);
|
|
1429
1807
|
if (current.manual_title || !stillAuto) {
|
|
1430
|
-
status('skipped', 'manual_title', effective);
|
|
1808
|
+
(await status('skipped', 'manual_title', effective));
|
|
1431
1809
|
return;
|
|
1432
1810
|
}
|
|
1433
1811
|
if (next !== effective) {
|
|
1434
1812
|
current.title = next;
|
|
1435
1813
|
markSessionTitleGenerated(current);
|
|
1436
|
-
this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1814
|
+
await this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1437
1815
|
// Python `sync_session_title` after generation: the state.db row follows when `sync_to_insights` is on.
|
|
1438
1816
|
await this.deps.syncTitle?.(current);
|
|
1439
1817
|
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
@@ -1442,11 +1820,11 @@ export class TurnRunner {
|
|
|
1442
1820
|
}
|
|
1443
1821
|
}
|
|
1444
1822
|
if (wrote) {
|
|
1445
|
-
status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
|
|
1446
|
-
put('title', { session_id: sessionId, title: effective });
|
|
1823
|
+
await status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
|
|
1824
|
+
await put('title', { session_id: sessionId, title: effective });
|
|
1447
1825
|
}
|
|
1448
1826
|
else
|
|
1449
|
-
status('skipped', source || 'unchanged', effective, rawPreview);
|
|
1827
|
+
(await status('skipped', source || 'unchanged', effective, rawPreview));
|
|
1450
1828
|
}
|
|
1451
1829
|
/**
|
|
1452
1830
|
* Python `_maybe_schedule_title_refresh` + `_run_background_title_refresh`: every N exchanges a generated title is
|
|
@@ -1463,7 +1841,7 @@ export class TurnRunner {
|
|
|
1463
1841
|
// Admission is released before title work, so an earlier turn's refresh may still be running: refreshes apply in
|
|
1464
1842
|
// turn order and each reads the title its predecessor wrote.
|
|
1465
1843
|
const sessionId = s.session_id;
|
|
1466
|
-
const run = (this.titleRefreshes.get(sessionId) ?? Promise.resolve()).catch(() => undefined).then(() => this.applyTitleRefresh(sessionId, s, userText, assistantText, put, status));
|
|
1844
|
+
const run = (this.titleRefreshes.get(sessionId) ?? Promise.resolve()).catch(() => undefined).then(async () => { await this.applyTitleRefresh(sessionId, s, userText, assistantText, put, status); });
|
|
1467
1845
|
this.titleRefreshes.set(sessionId, run);
|
|
1468
1846
|
try {
|
|
1469
1847
|
await run;
|
|
@@ -1474,44 +1852,48 @@ export class TurnRunner {
|
|
|
1474
1852
|
}
|
|
1475
1853
|
}
|
|
1476
1854
|
async applyTitleRefresh(sessionId, s, userText, assistantText, put, status) {
|
|
1477
|
-
const read = () => { try {
|
|
1478
|
-
return this.deps.store.get(sessionId);
|
|
1855
|
+
const read = async () => { try {
|
|
1856
|
+
return (await this.deps.store.get(sessionId));
|
|
1479
1857
|
}
|
|
1480
1858
|
catch {
|
|
1481
1859
|
return s;
|
|
1482
1860
|
} };
|
|
1483
|
-
const latest = read();
|
|
1861
|
+
const latest = (await read());
|
|
1484
1862
|
const placeholder = str(latest.title).trim();
|
|
1485
1863
|
if (['Untitled', 'New Chat', ''].includes(placeholder) || latest.manual_title || !latest.llm_title_generated)
|
|
1486
1864
|
return;
|
|
1487
1865
|
const cfg = await this.deps.profileConfig?.(latest.profile ?? null);
|
|
1488
1866
|
if (!cfg || !titleGenerationEnabled(cfg)) {
|
|
1489
|
-
status('refresh_skipped', cfg ? 'title_generation_disabled' : 'config_unavailable', placeholder);
|
|
1867
|
+
(await status('refresh_skipped', cfg ? 'title_generation_disabled' : 'config_unavailable', placeholder));
|
|
1490
1868
|
return;
|
|
1491
1869
|
}
|
|
1492
1870
|
const { title: next, status: llmStatus, rawPreview } = await this.llmTitle(latest, userText, assistantText);
|
|
1493
1871
|
if (!next) {
|
|
1494
|
-
status('refresh_skipped', llmStatus || 'empty', placeholder, rawPreview);
|
|
1872
|
+
(await status('refresh_skipped', llmStatus || 'empty', placeholder, rawPreview));
|
|
1495
1873
|
return;
|
|
1496
1874
|
}
|
|
1497
1875
|
const normalize = (value) => value.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1498
1876
|
if (normalize(next) === normalize(placeholder)) {
|
|
1499
|
-
status('refresh_skipped', 'same_title', placeholder, rawPreview);
|
|
1877
|
+
(await status('refresh_skipped', 'same_title', placeholder, rawPreview));
|
|
1500
1878
|
return;
|
|
1501
1879
|
}
|
|
1502
|
-
const current = read();
|
|
1880
|
+
const current = (await read());
|
|
1503
1881
|
// The user may have renamed the session while the title prompt ran.
|
|
1504
1882
|
if (current.manual_title || str(current.title).trim() !== placeholder) {
|
|
1505
|
-
status('skipped', 'manual_title', str(current.title).trim());
|
|
1883
|
+
(await status('skipped', 'manual_title', str(current.title).trim()));
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if ((await this.deps.service().isReadOnly(current))) {
|
|
1887
|
+
(await status('refresh_skipped', 'read_only', str(current.title).trim()));
|
|
1506
1888
|
return;
|
|
1507
1889
|
}
|
|
1508
1890
|
current.title = next;
|
|
1509
1891
|
markSessionTitleGenerated(current);
|
|
1510
|
-
this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1892
|
+
await this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1511
1893
|
await this.deps.syncTitle?.(current);
|
|
1512
1894
|
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
1513
|
-
status('refreshed', llmStatus, next, rawPreview);
|
|
1514
|
-
put('title', { session_id: sessionId, title: next });
|
|
1895
|
+
await status('refreshed', llmStatus, next, rawPreview);
|
|
1896
|
+
await put('title', { session_id: sessionId, title: next });
|
|
1515
1897
|
}
|
|
1516
1898
|
// ── cancel / steer ───────────────────────────────────────────────────────
|
|
1517
1899
|
/**
|
|
@@ -1519,6 +1901,26 @@ export class TurnRunner {
|
|
|
1519
1901
|
* `steer_withdrawn` (stopped) this Stop emitted, for the cancel response (TAL-426).
|
|
1520
1902
|
*/
|
|
1521
1903
|
async cancel(streamId) {
|
|
1904
|
+
const ownerCancelled = await this.deps.service().owner?.cancel(streamId);
|
|
1905
|
+
if (ownerCancelled !== null && ownerCancelled !== undefined)
|
|
1906
|
+
return { cancelled: ownerCancelled, withdrawn: [] };
|
|
1907
|
+
// TAL-899: the user's Stop holds automatic turns, even one that lands after `done`, unless a newer turn already
|
|
1908
|
+
// took the session: a late or retried Stop for a turn the user followed holds nothing.
|
|
1909
|
+
const owner = this.registry.ownerSessionId(streamId) ?? this.registry.activeRuns.get(streamId)?.session_id;
|
|
1910
|
+
if (owner && this.registry.writebackOwners.get(owner) === streamId) {
|
|
1911
|
+
this.stopHeld.add(owner);
|
|
1912
|
+
try {
|
|
1913
|
+
const session = (await this.deps.store.get(owner));
|
|
1914
|
+
session.automatic_turns_stop_held = true;
|
|
1915
|
+
await this.deps.store.save(session, { touchUpdatedAt: false });
|
|
1916
|
+
}
|
|
1917
|
+
catch (error) {
|
|
1918
|
+
this.deps.log(`[webui] WARNING: failed to persist Stop hold for session ${owner}; interrupting with the in-memory hold: ${error.message}`);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
return (await this.stop(streamId));
|
|
1922
|
+
}
|
|
1923
|
+
async stop(streamId) {
|
|
1522
1924
|
const channel = this.registry.peek(streamId);
|
|
1523
1925
|
const run = this.registry.activeRuns.get(streamId);
|
|
1524
1926
|
if (!channel && !run)
|
|
@@ -1553,24 +1955,24 @@ export class TurnRunner {
|
|
|
1553
1955
|
if (sessionId) {
|
|
1554
1956
|
let current = null;
|
|
1555
1957
|
try {
|
|
1556
|
-
current = this.deps.store.get(sessionId);
|
|
1958
|
+
current = (await this.deps.store.get(sessionId));
|
|
1557
1959
|
}
|
|
1558
1960
|
catch {
|
|
1559
1961
|
current = null;
|
|
1560
1962
|
}
|
|
1561
1963
|
if (current?.active_stream_id === streamId) {
|
|
1562
|
-
this.finalizeCancelled(current, streamId, run?.ephemeral, checkpoint);
|
|
1964
|
+
await this.finalizeCancelled(current, streamId, run?.ephemeral, checkpoint);
|
|
1563
1965
|
}
|
|
1564
1966
|
}
|
|
1565
1967
|
// The worker may already have written its terminal row while the interrupt was in flight; the stream is then
|
|
1566
1968
|
// gone from the registry and this path must not add a second one.
|
|
1567
1969
|
if (channel && this.registry.streams.has(streamId)) {
|
|
1568
1970
|
const writer = this.writers.get(streamId) ?? null;
|
|
1569
|
-
const emit = (event, data) => {
|
|
1971
|
+
const emit = async (event, data) => {
|
|
1570
1972
|
let eventId = null;
|
|
1571
1973
|
if (writer) {
|
|
1572
1974
|
try {
|
|
1573
|
-
eventId = writer.appendSseEvent(event, data).event_id;
|
|
1975
|
+
eventId = (await writer.appendSseEvent(event, data)).event_id;
|
|
1574
1976
|
}
|
|
1575
1977
|
catch {
|
|
1576
1978
|
eventId = null;
|
|
@@ -1579,8 +1981,10 @@ export class TurnRunner {
|
|
|
1579
1981
|
channel.put([event, data, eventId]);
|
|
1580
1982
|
};
|
|
1581
1983
|
for (const [event, data] of steerEvents)
|
|
1582
|
-
emit(event, data);
|
|
1583
|
-
|
|
1984
|
+
(await emit(event, data));
|
|
1985
|
+
this.runStatuses.delete(streamId);
|
|
1986
|
+
await emit('run_status', { session_id: sessionId, status: null });
|
|
1987
|
+
await emit('cancel', (await this.cancelFrame(sessionId)));
|
|
1584
1988
|
this.registry.streams.delete(streamId);
|
|
1585
1989
|
this.registry.liveIds.delete(streamId);
|
|
1586
1990
|
}
|
|
@@ -1596,7 +2000,7 @@ export class TurnRunner {
|
|
|
1596
2000
|
const streamId = this.registry.activeRunStreamForSession(sessionId);
|
|
1597
2001
|
if (!streamId || this.registry.activeRuns.get(streamId)?.origin !== 'background')
|
|
1598
2002
|
return false;
|
|
1599
|
-
await this.
|
|
2003
|
+
await this.stop(streamId);
|
|
1600
2004
|
let timer;
|
|
1601
2005
|
await Promise.race([this.workers.get(streamId), new Promise((resolve) => { timer = setTimeout(resolve, BACKGROUND_UNWIND_WAIT_MS); })]);
|
|
1602
2006
|
clearTimeout(timer);
|
|
@@ -1605,7 +2009,7 @@ export class TurnRunner {
|
|
|
1605
2009
|
async steer(sessionId, text, displayText, steerId) {
|
|
1606
2010
|
let s = null;
|
|
1607
2011
|
try {
|
|
1608
|
-
s = this.deps.store.get(sessionId, { metadataOnly: true });
|
|
2012
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
1609
2013
|
}
|
|
1610
2014
|
catch {
|
|
1611
2015
|
s = null;
|
|
@@ -1624,7 +2028,7 @@ export class TurnRunner {
|
|
|
1624
2028
|
return { accepted: false, fallback: 'no_cached_agent', stream_id: null };
|
|
1625
2029
|
// Register provisionally before the RPC: the Agent may apply the steer and finish the turn before the steer
|
|
1626
2030
|
// reply arrives, and the turn's finalisation must then already see the record. A rejection removes it again.
|
|
1627
|
-
return this.withSteerChain(activeStreamId, () => this.steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId));
|
|
2031
|
+
return this.withSteerChain(activeStreamId, sessionId, () => this.steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId));
|
|
1628
2032
|
}
|
|
1629
2033
|
async steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId) {
|
|
1630
2034
|
const record = { steer_id: steerId, session_id: sessionId, stream_id: activeStreamId, text, display_text: displayText || text, created_at: this.deps.now(), state: 'pending', can_redirect: false };
|
|
@@ -1656,7 +2060,7 @@ export class TurnRunner {
|
|
|
1656
2060
|
record.can_redirect = result.can_redirect === true;
|
|
1657
2061
|
// TAL-424: every client shows it until the Agent takes it (unless it already did while the reply was in flight).
|
|
1658
2062
|
if (this.steers.get(activeStreamId)?.includes(record))
|
|
1659
|
-
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
2063
|
+
(await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record)));
|
|
1660
2064
|
}
|
|
1661
2065
|
return { accepted: result.accepted, fallback: result.accepted ? null : (result.fallback ?? 'not_running'), stream_id: activeStreamId, steer_id: steerId };
|
|
1662
2066
|
}
|
|
@@ -1725,10 +2129,10 @@ export class TurnRunner {
|
|
|
1725
2129
|
return boundary;
|
|
1726
2130
|
}
|
|
1727
2131
|
/** A steer is saved as it enters the stream, so a reload mid-turn already has it; the terminal save re-places it. */
|
|
1728
|
-
saveConsumedSteers(sessionId, streamId) {
|
|
2132
|
+
async saveConsumedSteers(sessionId, streamId) {
|
|
1729
2133
|
let s;
|
|
1730
2134
|
try {
|
|
1731
|
-
s = this.deps.store.get(sessionId);
|
|
2135
|
+
s = (await this.deps.store.get(sessionId));
|
|
1732
2136
|
}
|
|
1733
2137
|
catch {
|
|
1734
2138
|
return;
|
|
@@ -1736,7 +2140,7 @@ export class TurnRunner {
|
|
|
1736
2140
|
if (s.active_stream_id !== streamId || this.placeConsumedSteers(s, streamId, s.pending_started_at) === null)
|
|
1737
2141
|
return;
|
|
1738
2142
|
try {
|
|
1739
|
-
this.deps.store.save(s, { touchUpdatedAt: false });
|
|
2143
|
+
(await this.deps.store.save(s, { touchUpdatedAt: false }));
|
|
1740
2144
|
}
|
|
1741
2145
|
catch (error) {
|
|
1742
2146
|
this.deps.log(`[webui] WARNING: failed to save consumed steer for ${sessionId}: ${error.message}`);
|
|
@@ -1796,18 +2200,18 @@ export class TurnRunner {
|
|
|
1796
2200
|
* TAL-424: the steers a finished turn never took go on as one follow-up turn, sent once by the server for every client.
|
|
1797
2201
|
* Should another turn already hold the session, they steer into that one instead, so no text is lost.
|
|
1798
2202
|
*/
|
|
1799
|
-
startSteerFollowUp(sessionId, texts) {
|
|
2203
|
+
async startSteerFollowUp(sessionId, texts) {
|
|
1800
2204
|
if (!texts.length)
|
|
1801
2205
|
return;
|
|
1802
2206
|
let s;
|
|
1803
2207
|
try {
|
|
1804
|
-
s = this.deps.store.get(sessionId);
|
|
2208
|
+
s = (await this.deps.store.get(sessionId));
|
|
1805
2209
|
}
|
|
1806
2210
|
catch {
|
|
1807
2211
|
return;
|
|
1808
2212
|
}
|
|
1809
2213
|
const msg = texts.join('\n');
|
|
1810
|
-
const started = this.
|
|
2214
|
+
const started = (await this.startServerTurn(s, { msg }));
|
|
1811
2215
|
if (!started._status || started._status < 400)
|
|
1812
2216
|
return;
|
|
1813
2217
|
if (started._status === 409 && started.active_stream_id) {
|
|
@@ -1835,61 +2239,83 @@ export class TurnRunner {
|
|
|
1835
2239
|
// Only a known goal turn shows progress up front; any other turn shows nothing unless a goal turns out to be active.
|
|
1836
2240
|
const goalTurn = this.registry.goalRelated.has(streamId);
|
|
1837
2241
|
if (goalTurn)
|
|
1838
|
-
put('goal', { session_id: sessionId, state: 'evaluating', message: 'Evaluating goal progress…', message_key: 'goal_evaluating_progress' });
|
|
2242
|
+
(await put('goal', { session_id: sessionId, state: 'evaluating', message: 'Evaluating goal progress…', message_key: 'goal_evaluating_progress' }));
|
|
1839
2243
|
let decision;
|
|
1840
2244
|
try {
|
|
1841
|
-
|
|
2245
|
+
// The workspace the turn ran in, unrecovered: the Agent pauses the goal rather than run its gates anywhere else.
|
|
2246
|
+
this.deps.service().assertWritable(s);
|
|
2247
|
+
decision = await sidecar.call('goals.evaluate', { session_id: sessionId, profile_home: this.deps.profileHome(s.profile), last_response: lastResponse, user_initiated: true, workspace: str(s.workspace) || undefined });
|
|
1842
2248
|
}
|
|
1843
2249
|
catch (error) {
|
|
1844
2250
|
this.deps.log(`[webui] WARNING: goal evaluation failed for ${sessionId}: ${error.message}`);
|
|
1845
2251
|
if (goalTurn)
|
|
1846
|
-
put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
2252
|
+
(await put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' }));
|
|
1847
2253
|
return;
|
|
1848
2254
|
}
|
|
2255
|
+
// The judgement moved the goal on (turns, park, pause, done); an inactive one only matters if a goal still shows.
|
|
2256
|
+
if (decision.verdict !== 'inactive' || this.goals.view(sessionId))
|
|
2257
|
+
await this.refreshGoal(s, put);
|
|
1849
2258
|
if (!goalTurn && decision.verdict === 'inactive')
|
|
1850
2259
|
return;
|
|
1851
2260
|
const prompt = str(decision.continuation_prompt).trim();
|
|
1852
2261
|
const proceed = decision.should_continue && Boolean(prompt);
|
|
1853
2262
|
const status = { message: decision.message, message_key: decision.message_key, message_args: decision.message_args ?? [], decision: decision.verdict };
|
|
1854
|
-
|
|
1855
|
-
|
|
2263
|
+
if (!proceed) {
|
|
2264
|
+
(await put('goal', { session_id: sessionId, state: 'idle', ...status }));
|
|
1856
2265
|
return;
|
|
2266
|
+
}
|
|
1857
2267
|
let current;
|
|
1858
2268
|
try {
|
|
1859
|
-
current = this.deps.store.get(sessionId);
|
|
2269
|
+
current = (await this.deps.store.get(sessionId));
|
|
1860
2270
|
}
|
|
1861
2271
|
catch {
|
|
1862
2272
|
return;
|
|
1863
2273
|
}
|
|
1864
|
-
const started = this.
|
|
2274
|
+
const started = (await this.startServerTurn(current, { msg: prompt, goalRelated: true }));
|
|
2275
|
+
// TAL-899: a Stop, even one after the answer, holds the continuation; the user's next turn is judged instead.
|
|
2276
|
+
if (started.held) {
|
|
2277
|
+
(await put('goal', { session_id: sessionId, state: 'idle', decision: 'held', message: '' }));
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
await put('goal', { session_id: sessionId, state: 'continuing', ...status });
|
|
1865
2281
|
if (started._status === 409 && started.active_stream_id) {
|
|
1866
2282
|
this.registry.goalRelated.add(started.active_stream_id);
|
|
1867
2283
|
return;
|
|
1868
2284
|
}
|
|
1869
2285
|
if (started._status !== undefined && started._status >= 400) {
|
|
1870
2286
|
this.deps.log(`[webui] WARNING: goal continuation for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
|
|
1871
|
-
put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
2287
|
+
await put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
1872
2288
|
return;
|
|
1873
2289
|
}
|
|
1874
|
-
put('goal_continue', { session_id: sessionId, state: 'continuing', continuation_prompt: prompt, text: prompt, stream_id: str(started.stream_id), ...status });
|
|
2290
|
+
await put('goal_continue', { session_id: sessionId, state: 'continuing', continuation_prompt: prompt, text: prompt, stream_id: str(started.stream_id), ...status });
|
|
1875
2291
|
}
|
|
1876
2292
|
/** The Agent's pending steer text settles which steers it has taken: those become `steer_consumed` and their rows. */
|
|
1877
|
-
settleConsumedSteers(sessionId, streamId, pendingText) {
|
|
2293
|
+
async settleConsumedSteers(sessionId, streamId, pendingText) {
|
|
1878
2294
|
for (const record of this.takeConsumedSteers(streamId, pendingText, { keepLeftovers: true }))
|
|
1879
|
-
this.emitToSession(sessionId, 'steer_consumed', record);
|
|
1880
|
-
this.saveConsumedSteers(sessionId, streamId);
|
|
2295
|
+
(await this.emitToSession(sessionId, 'steer_consumed', record));
|
|
2296
|
+
await this.saveConsumedSteers(sessionId, streamId);
|
|
1881
2297
|
}
|
|
1882
2298
|
/** What every client shows for a pending steer; the server decides which actions it offers. */
|
|
1883
2299
|
pendingSteerView(r) {
|
|
1884
2300
|
const open = r.state === 'pending';
|
|
1885
2301
|
return { steer_id: r.steer_id, text: r.display_text, submitted_at: r.created_at, state: r.state, actions: { edit: open, cancel: open, send_now: open && r.can_redirect } };
|
|
1886
2302
|
}
|
|
2303
|
+
runStatus(streamId) {
|
|
2304
|
+
return this.runStatuses.get(streamId) ?? null;
|
|
2305
|
+
}
|
|
2306
|
+
/** TAL-1006: the start (epoch seconds) of the stream's open compression span, else null. */
|
|
2307
|
+
compressionStartedAt(streamId) {
|
|
2308
|
+
return this.compressionSpans.get(streamId) ?? null;
|
|
2309
|
+
}
|
|
1887
2310
|
/** TAL-424: the active stream's pending steers, oldest first (session detail `pending_steers`). */
|
|
1888
2311
|
pendingSteers(streamId) {
|
|
1889
2312
|
return (this.steers.get(streamId) ?? []).map((r) => this.pendingSteerView(r));
|
|
1890
2313
|
}
|
|
1891
|
-
withSteerChain(streamId, fn) {
|
|
1892
|
-
const run = (this.steerChains.get(streamId) ?? Promise.resolve()).then(
|
|
2314
|
+
withSteerChain(streamId, sessionId, fn) {
|
|
2315
|
+
const run = (this.steerChains.get(streamId) ?? Promise.resolve()).then(async () => {
|
|
2316
|
+
await this.deps.service().getForMutation(sessionId);
|
|
2317
|
+
return fn();
|
|
2318
|
+
});
|
|
1893
2319
|
const settled = run.catch(() => undefined).then(() => { if (this.steerChains.get(streamId) === settled)
|
|
1894
2320
|
this.steerChains.delete(streamId); });
|
|
1895
2321
|
this.steerChains.set(streamId, settled);
|
|
@@ -1903,7 +2329,7 @@ export class TurnRunner {
|
|
|
1903
2329
|
async rewriteSteer(sessionId, steerId, fallback, rewrite) {
|
|
1904
2330
|
let s = null;
|
|
1905
2331
|
try {
|
|
1906
|
-
s = this.deps.store.get(sessionId, { metadataOnly: true });
|
|
2332
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
1907
2333
|
}
|
|
1908
2334
|
catch {
|
|
1909
2335
|
s = null;
|
|
@@ -1911,21 +2337,21 @@ export class TurnRunner {
|
|
|
1911
2337
|
const streamId = s?.active_stream_id;
|
|
1912
2338
|
const sidecar = this.deps.sidecar();
|
|
1913
2339
|
if (!streamId || !sidecar)
|
|
1914
|
-
return fallback;
|
|
1915
|
-
return this.withSteerChain(streamId, async () => {
|
|
2340
|
+
return fallback('not_running');
|
|
2341
|
+
return this.withSteerChain(streamId, sessionId, async () => {
|
|
1916
2342
|
const records = this.steers.get(streamId) ?? [];
|
|
1917
2343
|
const index = records.findIndex((r) => r.steer_id === steerId);
|
|
1918
2344
|
if (index < 0 || records[index].state !== 'pending')
|
|
1919
|
-
return fallback;
|
|
2345
|
+
return fallback('taken');
|
|
1920
2346
|
// In flight before the sidecar call, so a pending-text report racing its reply is deferred too.
|
|
1921
2347
|
let finished = () => undefined;
|
|
1922
2348
|
this.steerRewrites.set(streamId, new Promise((resolve) => { finished = resolve; }));
|
|
1923
|
-
let outcome = { result: fallback, changed: false };
|
|
2349
|
+
let outcome = { result: fallback('failed'), changed: false };
|
|
1924
2350
|
try {
|
|
1925
2351
|
outcome = await rewrite(sidecar, streamId, records, index);
|
|
1926
2352
|
}
|
|
1927
2353
|
catch {
|
|
1928
|
-
outcome = { result: fallback, changed: false };
|
|
2354
|
+
outcome = { result: fallback('failed'), changed: false };
|
|
1929
2355
|
}
|
|
1930
2356
|
finally {
|
|
1931
2357
|
this.steerRewrites.delete(streamId);
|
|
@@ -1933,7 +2359,7 @@ export class TurnRunner {
|
|
|
1933
2359
|
const deferred = this.deferredPendingSteer.get(streamId);
|
|
1934
2360
|
this.deferredPendingSteer.delete(streamId);
|
|
1935
2361
|
if (deferred !== undefined && !outcome.changed)
|
|
1936
|
-
this.settleConsumedSteers(sessionId, streamId, deferred);
|
|
2362
|
+
(await this.settleConsumedSteers(sessionId, streamId, deferred));
|
|
1937
2363
|
}
|
|
1938
2364
|
return outcome.result;
|
|
1939
2365
|
});
|
|
@@ -1948,54 +2374,61 @@ export class TurnRunner {
|
|
|
1948
2374
|
* TAL-424 Edit and Cancel: take a pending steer back before the Agent takes it. `withdrawn: false` (and nothing
|
|
1949
2375
|
* emitted) when it is unknown, already taken or being sent; a taken one then settles as consumed.
|
|
1950
2376
|
*/
|
|
1951
|
-
withdrawSteer(sessionId, steerId, reason) {
|
|
1952
|
-
return this.rewriteSteer(sessionId, steerId, { withdrawn: false }, async (sidecar, streamId, records, index) => {
|
|
2377
|
+
async withdrawSteer(sessionId, steerId, reason) {
|
|
2378
|
+
return (await this.rewriteSteer(sessionId, steerId, () => ({ withdrawn: false }), async (sidecar, streamId, records, index) => {
|
|
1953
2379
|
const record = records[index];
|
|
1954
2380
|
const reply = await sidecar.call('chat.steer_withdraw', { stream_id: streamId, pending: records.map((r) => r.text), index });
|
|
1955
2381
|
if (!reply.withdrawn)
|
|
1956
2382
|
return { result: { withdrawn: false }, changed: false };
|
|
1957
2383
|
this.dropSteer(streamId, record);
|
|
1958
|
-
this.emitToSession(sessionId, 'steer_withdrawn', { steer_id: record.steer_id, reason, text: record.display_text });
|
|
2384
|
+
await this.emitToSession(sessionId, 'steer_withdrawn', { steer_id: record.steer_id, reason, text: record.display_text });
|
|
1959
2385
|
return { result: { withdrawn: true, text: record.display_text }, changed: true };
|
|
1960
|
-
});
|
|
2386
|
+
}));
|
|
1961
2387
|
}
|
|
1962
2388
|
/**
|
|
1963
2389
|
* TAL-424 Send now: deliver a pending steer with the Agent's redirect. A model request restarts with it (consumed at
|
|
1964
2390
|
* once); during tools it goes last on the Agent's queue and stays `sending_now` until taken; with nothing live it
|
|
1965
2391
|
* stays pending.
|
|
1966
2392
|
*/
|
|
1967
|
-
sendSteerNow(sessionId, steerId) {
|
|
1968
|
-
return this.rewriteSteer(sessionId, steerId, { redirected: false }, async (sidecar, streamId, records, index) => {
|
|
2393
|
+
async sendSteerNow(sessionId, steerId) {
|
|
2394
|
+
return (await this.rewriteSteer(sessionId, steerId, (outcome) => ({ redirected: false, outcome }), async (sidecar, streamId, records, index) => {
|
|
1969
2395
|
const record = records[index];
|
|
1970
2396
|
if (!record.can_redirect)
|
|
1971
|
-
return { result: { redirected: false }, changed: false };
|
|
2397
|
+
return { result: { redirected: false, outcome: 'failed' }, changed: false };
|
|
1972
2398
|
const reply = await sidecar.call('chat.steer_now', { stream_id: streamId, pending: records.map((r) => r.text), index });
|
|
1973
2399
|
if (!reply.withdrawn)
|
|
1974
|
-
return { result: { redirected: false }, changed: false };
|
|
2400
|
+
return { result: { redirected: false, outcome: reply.reason === 'not_running' ? 'not_running' : 'taken' }, changed: false };
|
|
1975
2401
|
if (!reply.redirected) {
|
|
1976
2402
|
// The Agent's queue changed meanwhile, so it went back last: clients follow the new order.
|
|
1977
2403
|
if (reply.requeued === 'last') {
|
|
1978
2404
|
this.dropSteer(streamId, record);
|
|
1979
2405
|
records.push(record);
|
|
1980
|
-
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
2406
|
+
(await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record)));
|
|
1981
2407
|
}
|
|
1982
|
-
return { result: { redirected: false }, changed: true };
|
|
2408
|
+
return { result: { redirected: false, outcome: 'next_step' }, changed: true };
|
|
1983
2409
|
}
|
|
1984
2410
|
this.dropSteer(streamId, record);
|
|
1985
2411
|
if (reply.delivery === 'steer') {
|
|
1986
2412
|
record.state = 'sending_now';
|
|
1987
2413
|
records.push(record);
|
|
1988
|
-
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
2414
|
+
await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
1989
2415
|
}
|
|
1990
2416
|
else {
|
|
1991
|
-
this.emitToSession(sessionId, 'steer_consumed', this.consumedSteerPayload(record, { redirected: true }));
|
|
1992
|
-
this.saveConsumedSteers(sessionId, streamId);
|
|
2417
|
+
await this.emitToSession(sessionId, 'steer_consumed', this.consumedSteerPayload(record, { redirected: true }));
|
|
2418
|
+
await this.saveConsumedSteers(sessionId, streamId);
|
|
1993
2419
|
}
|
|
1994
|
-
return { result: { redirected: true }, changed: true };
|
|
1995
|
-
});
|
|
2420
|
+
return { result: { redirected: true, outcome: 'delivered' }, changed: true };
|
|
2421
|
+
}));
|
|
1996
2422
|
}
|
|
1997
2423
|
// ── approvals / clarify ──────────────────────────────────────────────────
|
|
1998
2424
|
async respondApproval(sessionId, choice, approvalId, enableYolo) {
|
|
2425
|
+
if (this.deps.service().owner?.isOwned(sessionId)) {
|
|
2426
|
+
if (enableYolo)
|
|
2427
|
+
throw new HttpFailure(403, 'Change approval policy through the owning Agent');
|
|
2428
|
+
await this.deps.service().owner.respond(sessionId, 'approval', approvalId, choice);
|
|
2429
|
+
return { ok: true, choice };
|
|
2430
|
+
}
|
|
2431
|
+
sessionId = (await this.deps.service().idForMutation(sessionId));
|
|
1999
2432
|
// Python `_enable_session_yolo_and_release_pending`: a stale card id while another approval is parked
|
|
2000
2433
|
// must not commit YOLO for the whole session — fail closed with the relay envelope instead.
|
|
2001
2434
|
if (enableYolo && approvalId && !this.deps.pending.hasApprovalId(sessionId, approvalId) && this.deps.pending.hasPendingApproval(sessionId)) {
|
|
@@ -2008,7 +2441,7 @@ export class TurnRunner {
|
|
|
2008
2441
|
const relayFailure = (message) => ({ ok: false, choice, error: `The Agent sidecar did not accept the approval (${message}); retry in a moment.`, _status: 503 });
|
|
2009
2442
|
let s = null;
|
|
2010
2443
|
try {
|
|
2011
|
-
s = this.deps.store.get(sessionId, { metadataOnly: true });
|
|
2444
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
2012
2445
|
}
|
|
2013
2446
|
catch {
|
|
2014
2447
|
s = null;
|
|
@@ -2050,7 +2483,7 @@ export class TurnRunner {
|
|
|
2050
2483
|
return relayFailure(str(error.message));
|
|
2051
2484
|
}
|
|
2052
2485
|
}
|
|
2053
|
-
this.deps.service().setYolo(sessionId, true);
|
|
2486
|
+
await this.deps.service().setYolo(sessionId, true);
|
|
2054
2487
|
return null;
|
|
2055
2488
|
});
|
|
2056
2489
|
if (failure)
|
|
@@ -2084,17 +2517,21 @@ export class TurnRunner {
|
|
|
2084
2517
|
return;
|
|
2085
2518
|
}
|
|
2086
2519
|
if (this.deps.pending.dropApprovals(sessionId, new Set(before.filter((id) => !live.has(id)))).length)
|
|
2087
|
-
this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId));
|
|
2520
|
+
(await this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId)));
|
|
2088
2521
|
}
|
|
2089
2522
|
withYoloLock(sessionId, fn) {
|
|
2090
|
-
const run = (this.yoloChains.get(sessionId) ?? Promise.resolve()).then(
|
|
2523
|
+
const run = (this.yoloChains.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
2524
|
+
sessionId = (await this.deps.service().idForMutation(sessionId));
|
|
2525
|
+
return fn();
|
|
2526
|
+
});
|
|
2091
2527
|
const settled = run.catch(() => undefined).then(() => { if (this.yoloChains.get(sessionId) === settled)
|
|
2092
2528
|
this.yoloChains.delete(sessionId); });
|
|
2093
2529
|
this.yoloChains.set(sessionId, settled);
|
|
2094
2530
|
return run;
|
|
2095
2531
|
}
|
|
2096
2532
|
/** Python `set_session_yolo_enabled(False)`: the sidecar must drop its per-session YOLO state before the local flag clears. */
|
|
2097
|
-
disableYolo(sessionId) {
|
|
2533
|
+
async disableYolo(sessionId) {
|
|
2534
|
+
sessionId = (await this.deps.service().idForMutation(sessionId));
|
|
2098
2535
|
return this.withYoloLock(sessionId, async () => {
|
|
2099
2536
|
const sidecar = this.deps.sidecar();
|
|
2100
2537
|
if (sidecar) {
|
|
@@ -2105,7 +2542,7 @@ export class TurnRunner {
|
|
|
2105
2542
|
return { ok: false, error: `The Agent sidecar did not disable YOLO (${str(error.message)}); retry in a moment.`, yolo_enabled: this.deps.service().yolo(sessionId).yolo_enabled === true, _status: 503 };
|
|
2106
2543
|
}
|
|
2107
2544
|
}
|
|
2108
|
-
return this.deps.service().setYolo(sessionId, false);
|
|
2545
|
+
return (await this.deps.service().setYolo(sessionId, false));
|
|
2109
2546
|
});
|
|
2110
2547
|
}
|
|
2111
2548
|
/**
|
|
@@ -2114,16 +2551,25 @@ export class TurnRunner {
|
|
|
2114
2551
|
* did not acknowledge (prompt retained).
|
|
2115
2552
|
*/
|
|
2116
2553
|
async respondClarify(sessionId, clarifyId, reply) {
|
|
2554
|
+
if (this.deps.service().owner?.isOwned(sessionId)) {
|
|
2555
|
+
await this.deps.service().owner.respond(sessionId, 'clarify', clarifyId, reply);
|
|
2556
|
+
return { ok: true };
|
|
2557
|
+
}
|
|
2558
|
+
sessionId = (await this.deps.service().idForMutation(sessionId));
|
|
2117
2559
|
const entry = this.deps.pending.peekClarify(sessionId, clarifyId);
|
|
2118
2560
|
if (!entry)
|
|
2119
2561
|
return { ok: false, stale: true };
|
|
2120
|
-
|
|
2562
|
+
// TAL-887: a password-manager answer goes to the Agent and nowhere else; `null` (a decline) is its empty reply.
|
|
2563
|
+
const vault = isVaultPrompt(entry);
|
|
2564
|
+
if (vault && typeof reply === 'string')
|
|
2565
|
+
return { ok: false, invalid: true };
|
|
2566
|
+
const response = vault ? vaultReply(entry, reply) : reply === null ? null : clarifyReply(entry, reply);
|
|
2121
2567
|
if (response === null)
|
|
2122
2568
|
return { ok: false, invalid: true };
|
|
2123
2569
|
// A test-hook prompt has no parked Agent waiter: it is answered here.
|
|
2124
2570
|
if (entry._injected === true) {
|
|
2125
2571
|
this.deps.pending.resolveClarify(sessionId, str(entry.clarify_id));
|
|
2126
|
-
return { ok: true, response };
|
|
2572
|
+
return vault ? { ok: true } : { ok: true, response };
|
|
2127
2573
|
}
|
|
2128
2574
|
const sidecar = this.deps.sidecar();
|
|
2129
2575
|
if (!sidecar)
|
|
@@ -2133,14 +2579,16 @@ export class TurnRunner {
|
|
|
2133
2579
|
ok = (await sidecar.call('clarify.respond', { session_id: sessionId, clarify_id: str(entry.clarify_id), response })).ok;
|
|
2134
2580
|
}
|
|
2135
2581
|
catch (error) {
|
|
2582
|
+
if (vault)
|
|
2583
|
+
return { ok: false, error: 'The Agent sidecar did not accept the secure answer; retry in a moment.' };
|
|
2136
2584
|
return { ok: false, error: `The Agent sidecar did not accept the answer (${str(error.message)}); retry in a moment.` };
|
|
2137
2585
|
}
|
|
2138
2586
|
if (!ok)
|
|
2139
2587
|
return { ok: false, stale: true };
|
|
2140
2588
|
const { head } = this.deps.pending.resolveClarify(sessionId, str(entry.clarify_id));
|
|
2141
2589
|
if (head)
|
|
2142
|
-
this.emitToSession(sessionId, 'clarify', head);
|
|
2143
|
-
return { ok: true, response };
|
|
2590
|
+
(await this.emitToSession(sessionId, 'clarify', head));
|
|
2591
|
+
return vault ? { ok: true } : { ok: true, response };
|
|
2144
2592
|
}
|
|
2145
2593
|
}
|
|
2146
2594
|
/** The turn's answer: the last non-error assistant reply with content. */
|