@maudecode/talaria-web 1.17.2 → 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.d.ts +1 -1
- package/dist/api/auth-raw.js +20 -20
- package/dist/api/auth-raw.js.map +1 -1
- package/dist/api/automation-raw.d.ts +7 -1
- package/dist/api/automation-raw.js +74 -27
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +128 -23
- package/dist/api/automation-router.js +29 -13
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +1845 -40
- package/dist/api/chat-router.js +196 -114
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +37 -3
- package/dist/api/git-router.js +47 -48
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +9 -1
- package/dist/api/raw-routes.js +286 -73
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +16 -6
- package/dist/api/router.js +28 -26
- 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 +3312 -131
- package/dist/api/sessions-router.js +287 -157
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +457 -8
- package/dist/api/settings-router.js +54 -33
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +7 -2
- package/dist/api/sse-routes.js +109 -62
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.d.ts +5 -0
- package/dist/api/tools-raw.js +63 -55
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +998 -85
- package/dist/api/tools-router.js +447 -61
- 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 +98 -59
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +15 -11
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/oidc.d.ts +5 -2
- package/dist/auth/oidc.js +8 -7
- package/dist/auth/oidc.js.map +1 -1
- package/dist/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 +15 -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 +116 -60
- package/dist/config/agent-config.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +19 -6
- 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 +31 -12
- package/dist/http/context.js +60 -24
- package/dist/http/context.js.map +1 -1
- package/dist/http/csp.d.ts +2 -1
- package/dist/http/csp.js +3 -2
- package/dist/http/csp.js.map +1 -1
- package/dist/onboarding.d.ts +2 -2
- package/dist/onboarding.js +10 -8
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.d.ts +20 -4
- package/dist/profiles/profiles.js +115 -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 +84 -14
- package/dist/providers/catalog.js +474 -151
- 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/raster-image.d.ts +7 -0
- package/dist/raster-image.js +267 -0
- package/dist/raster-image.js.map +1 -0
- package/dist/redact.js +342 -46
- 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 +305 -96
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +15 -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 -27
- package/dist/sessions/background-tasks.js +89 -97
- 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 +27 -6
- package/dist/sessions/cli-sessions.js +152 -42
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +77 -9
- package/dist/sessions/completions.js +482 -45
- 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/export.d.ts +5 -0
- package/dist/sessions/export.js +30 -0
- package/dist/sessions/export.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/gateway-watcher.d.ts +7 -3
- package/dist/sessions/gateway-watcher.js +31 -19
- package/dist/sessions/gateway-watcher.js.map +1 -1
- package/dist/sessions/goals.d.ts +31 -0
- package/dist/sessions/goals.js +75 -0
- package/dist/sessions/goals.js.map +1 -0
- package/dist/sessions/handoff.d.ts +32 -0
- package/dist/sessions/handoff.js +114 -0
- package/dist/sessions/handoff.js.map +1 -0
- package/dist/sessions/journal.d.ts +5 -79
- package/dist/sessions/journal.js +15 -606
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +31 -16
- package/dist/sessions/list.js +139 -129
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +69 -13
- package/dist/sessions/merge.js +265 -64
- 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 +19 -0
- package/dist/sessions/recovery.js +322 -0
- package/dist/sessions/recovery.js.map +1 -0
- package/dist/sessions/relay.d.ts +38 -19
- package/dist/sessions/relay.js +302 -79
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +260 -55
- package/dist/sessions/service.js +1448 -462
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +24 -2
- package/dist/sessions/session.js +69 -7
- 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.d.ts +1 -0
- package/dist/sessions/state-db-worker.js +35 -0
- package/dist/sessions/state-db-worker.js.map +1 -0
- package/dist/sessions/state-db.d.ts +113 -7
- package/dist/sessions/state-db.js +861 -44
- 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 +65 -68
- package/dist/sessions/store.js +305 -602
- 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.d.ts +2 -0
- package/dist/sessions/titles.js +5 -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 +30 -50
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +11 -2
- package/dist/sessions/turn-context.js +18 -11
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +122 -17
- package/dist/sessions/turn.js +939 -358
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/wakeup-pause.d.ts +54 -0
- package/dist/sessions/wakeup-pause.js +163 -0
- package/dist/sessions/wakeup-pause.js.map +1 -0
- 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 +178 -184
- 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.d.ts +2 -2
- package/dist/sidecar/fake.js +19 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/startup.d.ts +13 -5
- package/dist/startup.js +48 -0
- package/dist/startup.js.map +1 -1
- package/dist/test/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/test/ts-worker-hooks.d.ts +1 -0
- package/dist/test/ts-worker-hooks.js +24 -0
- package/dist/test/ts-worker-hooks.js.map +1 -0
- package/dist/text/diff.d.ts +9 -14
- package/dist/text/diff.js +74 -108
- package/dist/text/diff.js.map +1 -1
- package/dist/tools/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 +122 -18
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +53 -0
- package/dist/tools/dashboard-plugins.js +188 -0
- package/dist/tools/dashboard-plugins.js.map +1 -0
- package/dist/tools/extensions.d.ts +18 -5
- package/dist/tools/extensions.js +344 -266
- 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 +44 -0
- package/dist/tools/health.js +201 -46
- 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/mcp.d.ts +13 -0
- package/dist/tools/mcp.js +88 -0
- package/dist/tools/mcp.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/project-os.d.ts +11 -0
- package/dist/tools/project-os.js +267 -0
- package/dist/tools/project-os.js.map +1 -0
- package/dist/tools/skills.d.ts +14 -5
- package/dist/tools/skills.js +49 -16
- 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 +110 -32
- package/dist/tools/updates.js +1112 -294
- package/dist/tools/updates.js.map +1 -1
- package/dist/tools/wiki.d.ts +68 -0
- package/dist/tools/wiki.js +413 -0
- package/dist/tools/wiki.js.map +1 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +5 -0
- package/dist/util.js.map +1 -1
- package/dist/workspace/escape.d.ts +61 -0
- package/dist/workspace/escape.js +149 -0
- package/dist/workspace/escape.js.map +1 -0
- package/dist/workspace/extract.d.ts +22 -0
- package/dist/workspace/extract.js +308 -0
- package/dist/workspace/extract.js.map +1 -0
- package/dist/workspace/fs.d.ts +12 -0
- package/dist/workspace/fs.js +92 -23
- package/dist/workspace/fs.js.map +1 -1
- package/dist/workspace/git.d.ts +34 -1
- package/dist/workspace/git.js +328 -35
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media.d.ts +16 -13
- package/dist/workspace/media.js +43 -61
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/rollback.d.ts +36 -22
- package/dist/workspace/rollback.js +178 -272
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/unzip.js +4 -1
- package/dist/workspace/unzip.js.map +1 -1
- package/dist/workspace/upload.d.ts +7 -4
- package/dist/workspace/upload.js +5 -2
- package/dist/workspace/upload.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.d.ts +2 -1
- package/dist/workspace/worktrees.js +179 -42
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +7 -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 +32 -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 +56 -3
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +166 -24
- package/sidecar/talaria_sidecar/methods/chat.py +852 -135
- package/sidecar/talaria_sidecar/methods/checkpoints.py +89 -0
- package/sidecar/talaria_sidecar/methods/commands.py +294 -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 +395 -36
- package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
- package/sidecar/talaria_sidecar/methods/gateway.py +212 -55
- 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 +108 -34
- package/sidecar/talaria_sidecar/methods/mcp.py +1 -1
- 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 +218 -15
- package/sidecar/talaria_sidecar/methods/profiles.py +121 -13
- package/sidecar/talaria_sidecar/methods/providers.py +217 -28
- 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 +316 -75
- 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 +98 -13
- package/sidecar/talaria_sidecar/speed.py +35 -0
- package/static/dist/FILES.txt +126 -126
- 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-opyh9y0v.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-bfd4h3id.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-b2x1lqot.js → abnfDiagram-VCTEODGH-gjayq3ra.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-lp56972r.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-h38o5l7b.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-bqquxv2u.js → blockDiagram-I7D4REHJ-joedghi3.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-f3fb8w6z.js → c4Diagram-7LVT6UL2-hyrjrohe.js} +1 -1
- package/static/dist/assets/channel-es95o4y8.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-bzfyyzp3.js → chunk-4HAMMTFA-koouqbut.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-ly99rfx5.js → chunk-75Z2AOVW-bl4bhdnt.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-bg6ecfqf.js → chunk-DU6HZSFF-gpedttsp.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-e8k7mmqw.js → chunk-F27PBJKO-d8xxi0zi.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-cmalnoaj.js → chunk-GMAD6QVW-jro0hl73.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-gmif5o01.js → chunk-GVQU2GXP-djn6gbyu.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-f1nwwh6u.js → chunk-IMKFNOWR-vw4p5p7o.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-oktfh5v7.js → chunk-L3NEJ4N5-n1hj77q8.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-b1sdx96z.js → chunk-OSK3NFVY-oy0gwn2n.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-fhu4nzxt.js → chunk-P2QGCYS3-onf1hz6c.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-em31rnef.js → chunk-POPQ4Y6H-m8sqn7cs.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-bxhhyftw.js → chunk-PWAF6VOD-ek4smc8x.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-fmrvwcav.js → chunk-SHT3W25Y-g9gfvefo.js} +3 -3
- package/static/dist/assets/{chunk-SVP7TREG-lymukl2j.js → chunk-SVP7TREG-f9nfv2cu.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-lmza6k23.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-fw5b1pc6.js → copy-jamtd1pf.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-czlhbyhs.js → cynefin-OW5HDTMX-f8xfdd3f.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-gq7mvswb.js → cynefinDiagram-5FMLGOSQ-n9lb99h9.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-cqdbf7dr.js → dagre-GXQ25YYZ-dgailddi.js} +4 -4
- package/static/dist/assets/{diagram-S7CK7UJ4-o6arxa58.js → diagram-S7CK7UJ4-m6aeoe7c.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-nhnd68tz.js → diagram-UQ7AKVKN-jall3j0x.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-dk2sclzn.js → diagram-VSXAHHWV-hbawi7hn.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-n3k5nyx3.js → diagram-VX7I27RA-hendl2tt.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-m6q9cwdh.js → diagram-Z3DM3KII-igwc35p3.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-lsa5641p.js → ebnfDiagram-PWID7BFC-c64knz08.js} +1 -1
- package/static/dist/assets/{erDiagram-RLTQ6QDP-fo2ckbe6.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-nzqz6x9g.js → ganttDiagram-EL5Y4UJY-ma6jxj5i.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-gukvcxe6.js → gitGraph-4MIJSDKK-naa9w6xa.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-lqhdp8f0.js → gitGraphDiagram-WWUBYQGX-hzltvsod.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-igw6cp39.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-gvu7a93l.js → info-A6RAGUB7-mn2ut5ko.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-m8t8g6w1.js → infoDiagram-27XIBGKW-h6kuqvsb.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-l07vkbgj.js → ishikawaDiagram-5VMMS53U-e7bnwq7h.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-hybkgj8l.js → journeyDiagram-3NMN7TZE-etd1bi6p.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-by13uvtn.js → kanban-definition-UXKFOSKX-emb1g9dz.js} +4 -4
- package/static/dist/assets/lazyRouteComponent-o816ytzl.js +1 -0
- package/static/dist/assets/{line-osxq6fq7.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-i91oxmou.js → mermaid-parser.core-bnre27u8.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-m82mx8ww.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-nbtgge2o.js → packet-AYTQ26CC-hatq7m2e.js} +1 -1
- package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
- package/static/dist/assets/{pegDiagram-XKGWAZYB-f1zxzga1.js → pegDiagram-XKGWAZYB-gwisz8jv.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-of8vuhxs.js → pie-WAS4IAKB-fpftf0mk.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-o7y8ftio.js → pieDiagram-E7YTZNPT-khm1o9a6.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-iqiwia1t.js → quadrantDiagram-AXDQQJYC-owqc4uqn.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-delnsy2v.js → radar-RG4KPBEZ-cntfwjpq.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-nspnyq9u.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-z41tqx5z.js → railroadDiagram-O6MQD6OU-etuh0qa0.js} +1 -1
- package/static/dist/assets/{refresh-cw-nt1dtsx4.js → refresh-cw-mqbkv2a3.js} +1 -1
- package/static/dist/assets/refresh-l29ljp76.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-bdvif194.js → requirementDiagram-BXWQKSXE-oq2ulk22.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-h73e0rol.js → sankeyDiagram-P5KCCOFB-iqc02emv.js} +1 -1
- package/static/dist/assets/saved-ndd4t2xb.js +1 -0
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-iv95mb1z.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-k417ixmf.js → stateDiagram-D77RDMKH-dnpt1ato.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-bxjacpk6.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-esola70w.js → swimlanes-42K2YHIH-loqbdazb.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-id62p9hz.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-getrz2y8.js → timeline-definition-24CTP7MA-ol2hnvma.js} +1 -1
- package/static/dist/assets/toast-i3h34v6o.js +64 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-hc7ap1q7.js → treeView-Q6P3EWNA-idrj6iox.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-b7k1syy5.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-lhg566ew.js → useParams-gjc70qru.js} +1 -1
- package/static/dist/assets/{useSearch-lbrh6ejn.js → useSearch-e6ae539u.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-n6pcy5qb.js → vennDiagram-4TSXK5OY-bvwr9o2z.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-lpa18vz1.js → wardley-WFR3VGLG-31ui8tma.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-kpclwwg7.js → wardleyDiagram-VM6X3IG4-k5wqtx4b.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-paa2n8y0.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-fb2td9zi.js +0 -1
- package/static/dist/assets/ChatPage-cj96kiy9.js +0 -25
- package/static/dist/assets/Dialog-kxcg7ard.js +0 -1
- package/static/dist/assets/ExtensionRoute-jf7h0754.js +0 -1
- package/static/dist/assets/HubRoute-gmanpd5y.js +0 -2
- package/static/dist/assets/Markdown-mdwlvj3w.js +0 -598
- package/static/dist/assets/RightPanel-ftvrjlcp.js +0 -1
- package/static/dist/assets/Select-bilaypej.js +0 -1
- package/static/dist/assets/SessionListPanel-og8n36qs.js +0 -1
- package/static/dist/assets/SettingsLayout-jonjefmj.js +0 -1
- package/static/dist/assets/States-nmejrwrm.js +0 -1
- package/static/dist/assets/Toaster-nnl8zkt9.js +0 -1
- package/static/dist/assets/_app-gdidzd94.js +0 -1
- package/static/dist/assets/_app.index-hbarzbjx.js +0 -1
- package/static/dist/assets/_app.insights-luywlarz.js +0 -1
- package/static/dist/assets/_app.kanban-kincnbr8.js +0 -1
- package/static/dist/assets/_app.logs-oi2gr3gs.js +0 -1
- package/static/dist/assets/_app.memory-b6jfivt6.js +0 -1
- package/static/dist/assets/_app.profiles-lpvixmqp.js +0 -1
- package/static/dist/assets/_app.settings-vowx3iq0.js +0 -1
- package/static/dist/assets/_app.settings._section-cnntg1tu.js +0 -3
- package/static/dist/assets/_app.skills-fb57lpau.js +0 -1
- package/static/dist/assets/_app.tasks-eszpx1q7.js +0 -2
- package/static/dist/assets/_app.todos-nr05ac6w.js +0 -1
- package/static/dist/assets/_app.workspaces-d2luomu6.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-hd0o7bde.js +0 -1
- package/static/dist/assets/back-bemaer09.js +0 -1
- package/static/dist/assets/cancel-drd0g6d9.js +0 -1
- package/static/dist/assets/channel-cpdgf867.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-ko09sguk.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-ko09sguk.js +0 -1
- package/static/dist/assets/copied-ima3pysb.js +0 -1
- package/static/dist/assets/endpoints-d33siokx.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-emctlo64.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-bfi3moqa.js +0 -1
- package/static/dist/assets/index-cxq58xb1.js +0 -11
- package/static/dist/assets/index-hgtx08n5.css +0 -2
- package/static/dist/assets/lazyRouteComponent-af0lvgjx.js +0 -1
- package/static/dist/assets/link-jyxicdhg.js +0 -1
- package/static/dist/assets/login-ec21t39x.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-gfl8z3ep.js +0 -1
- package/static/dist/assets/onboarding-btnltvyz.js +0 -1
- package/static/dist/assets/onboarding_model_label-dp7xffoh.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nst1qxyn.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-ci37x8qy.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-aimkk07z.js +0 -1
- package/static/dist/assets/refresh-b4uams1i.js +0 -1
- package/static/dist/assets/saved-kg52csy7.js +0 -1
- package/static/dist/assets/settings_section_extensions_title-ej0iapxe.js +0 -1
- package/static/dist/assets/share._token-f97e94r0.js +0 -2
- package/static/dist/assets/sliders-horizontal-fj18it47.js +0 -1
- package/static/dist/assets/sse-fwpqtzbg.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hwn4apct.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d1h97oyg.js +0 -8
- package/static/dist/assets/todoStore-eginf9h4.js +0 -1
- package/static/dist/assets/tts-lxdxnmjd.js +0 -1
- package/static/dist/assets/untitled-ovpfthg4.js +0 -1
- package/static/dist/assets/useForm-bcblv1rx.js +0 -1
- package/static/dist/assets/useLocale-fi5jb8ma.js +0 -1
package/dist/sessions/turn.js
CHANGED
|
@@ -6,37 +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 { 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,
|
|
30
|
-
import { mayBecomeSilentReply, turnOrigin,
|
|
31
|
-
import { fallbackTitleFromExchange, firstExchangeSnippets, isGenericFallbackTitle, latestExchangeSnippets, looksInvalidGeneratedTitle, looksLikeDefaultCliTitle, sanitizeGeneratedTitle, titleLanguageMismatch, titlePrompts } from './titles.js';
|
|
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';
|
|
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';
|
|
34
|
+
import { CHECKPOINT_STORE_BUSY_MESSAGE } from '../workspace/rollback.js';
|
|
33
35
|
import { str } from '../util.js';
|
|
34
36
|
import { toolEditDiff } from './tool-display.js';
|
|
37
|
+
import { recordWakeupPause } from './wakeup-pause.js';
|
|
38
|
+
import { displayName } from '../providers/catalog.js';
|
|
35
39
|
export const CHAT_LOCK_WAIT_SECONDS = 2;
|
|
36
40
|
const IMAGE_MODE_TIMEOUT_MS = 15_000;
|
|
37
41
|
const TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT = 80;
|
|
38
42
|
/** TAL-397: streamed text reports the live meter at most this often (seconds); a counter change reports at once. */
|
|
39
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']);
|
|
40
46
|
/** How long a user's message waits for a stopped background turn to unwind before its own turn is admitted. */
|
|
41
47
|
const BACKGROUND_UNWIND_WAIT_MS = 30_000;
|
|
42
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.' };
|
|
@@ -63,6 +69,9 @@ export function classifyProviderError(errStr, opts = {}) {
|
|
|
63
69
|
const lower = errStr.toLowerCase();
|
|
64
70
|
if (opts.compressionExhausted)
|
|
65
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.' };
|
|
66
75
|
if (opts.condition === 'credential_missing')
|
|
67
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.' };
|
|
68
77
|
const cancelled = ['cancelled by user', 'canceled by user', 'user cancelled', 'user canceled', 'task cancelled', 'task canceled', 'cancellederror'].some((k) => lower.includes(k));
|
|
@@ -71,7 +80,7 @@ export function classifyProviderError(errStr, opts = {}) {
|
|
|
71
80
|
if (['interrupted by user', 'response interrupted', 'operation interrupted', 'operation was interrupted', 'operation aborted', 'request was aborted', 'aborterror'].some((k) => lower.includes(k))) {
|
|
72
81
|
return { label: 'Response interrupted', type: 'interrupted', hint: 'The run stopped before a provider response completed. If you did not cancel it, try again.' };
|
|
73
82
|
}
|
|
74
|
-
const quota = ['insufficient_quota', 'insufficient credits', 'out of credits', 'credits exhausted', 'quota exceeded', 'usage limit', 'plan limit', 'billing'].some((k) => lower.includes(k));
|
|
83
|
+
const quota = ['insufficient_quota', 'insufficient credits', 'out of credits', 'credits exhausted', 'quota exceeded', 'usage limit', 'plan limit', 'billing', 'more credits', 'can only afford', 'fewer max_tokens', 'credit balance', 'exceeded your current quota', 'usage_limit_exceeded', 'reached the limit of messages', 'used up your usage'].some((k) => lower.includes(k));
|
|
75
84
|
const poolEmpty = lower.includes('credential(s) exhausted') || lower.includes('credentials exhausted') || (lower.includes('credential') && lower.includes('exhausted'));
|
|
76
85
|
const auth = !quota && !poolEmpty && (errStr.includes('401') || lower.includes('authentication') || lower.includes('unauthorized') || lower.includes('invalid api key') || lower.includes('invalid_api_key') || lower.includes('no cookie auth credentials'));
|
|
77
86
|
const notFound = errStr.includes('404') || lower.includes('not found') || lower.includes('does not exist') || lower.includes('model_not_found') || lower.includes('invalid model') || lower.includes('does not match any known model') || lower.includes('unknown model');
|
|
@@ -125,8 +134,6 @@ export function sniffImageMime(bytes, declared = '') {
|
|
|
125
134
|
return 'image/bmp';
|
|
126
135
|
return null;
|
|
127
136
|
}
|
|
128
|
-
const COMPRESSION_RECOVERY_TERMINAL_STATE = 'compression_exhausted';
|
|
129
|
-
const COMPRESSION_RECOVERY_ACTION_START_FOCUSED = 'start_focused_continuation';
|
|
130
137
|
/** Python `build_compression_recovery_payload` + `stamp_compression_exhausted_recovery`. */
|
|
131
138
|
export function stampCompressionExhaustedRecovery(s, message, details) {
|
|
132
139
|
const positive = (v) => { const n = Math.trunc(Number(v) || 0); return n > 0 ? n : 0; };
|
|
@@ -209,8 +216,14 @@ 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();
|
|
225
|
+
/** The last adaptive title refresh per session; a successor turn's refresh runs after it (TAL-589). */
|
|
226
|
+
titleRefreshes = new Map();
|
|
214
227
|
/**
|
|
215
228
|
* TAL-364: what a Stop needs to write the turn's model context: its starting context, the prompt the Agent got, and
|
|
216
229
|
* whether a settlement already used a canonical checkpoint (which marks the stop boundary; later work never replaces it).
|
|
@@ -220,25 +233,134 @@ export class TurnRunner {
|
|
|
220
233
|
stopContexts = new Map();
|
|
221
234
|
/** Each turn's worker, so a stopped background turn can be awaited before the user's turn takes the session. */
|
|
222
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());
|
|
223
240
|
constructor(deps) {
|
|
224
241
|
this.deps = deps;
|
|
225
242
|
}
|
|
226
243
|
get registry() { return this.deps.registry; }
|
|
227
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
|
+
}
|
|
228
345
|
/** Python `_start_chat_stream_for_session`: persist pending state, register the channel, launch the worker. */
|
|
229
|
-
|
|
346
|
+
async admit(session, opts, releaseStopHold = false) {
|
|
230
347
|
const s = session;
|
|
348
|
+
const readOnly = (await this.deps.service().readOnlyReason(s));
|
|
349
|
+
if (readOnly)
|
|
350
|
+
return { error: readOnly, _status: 403 };
|
|
231
351
|
if (this.deps.updateInProgress?.())
|
|
232
352
|
return { error: 'Web is updating. Retry after it restarts.', _status: 503 };
|
|
233
353
|
if (this.deps.profileDeleting?.(s.profile ?? null))
|
|
234
354
|
return { error: `Profile '${str(s.profile)}' is being deleted.`, _status: 409 };
|
|
235
355
|
if (this.deps.workspaceBusy?.(opts.workspace))
|
|
236
356
|
return { error: WORKSPACE_BUSY_MESSAGE, _status: 409 };
|
|
357
|
+
if (this.deps.checkpointStoreBusy?.(s.profile ?? null))
|
|
358
|
+
return { error: CHECKPOINT_STORE_BUSY_MESSAGE, _status: 409 };
|
|
237
359
|
const locked = s.active_stream_id;
|
|
238
360
|
if (locked) {
|
|
239
361
|
if (this.registry.liveIds.has(locked) || this.registry.activeRuns.has(locked))
|
|
240
362
|
return { error: 'session already has an active stream', active_stream_id: locked, _status: 409 };
|
|
241
|
-
this.deps.service().clearStaleStreamState(s);
|
|
363
|
+
await this.deps.service().clearStaleStreamState(s);
|
|
242
364
|
if (s.active_stream_id)
|
|
243
365
|
return { error: 'session already has an active stream', active_stream_id: s.active_stream_id, _status: 409 };
|
|
244
366
|
}
|
|
@@ -247,11 +369,21 @@ export class TurnRunner {
|
|
|
247
369
|
if (blocking)
|
|
248
370
|
return { error: 'session already has an active stream', active_stream_id: blocking, _status: 409 };
|
|
249
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
|
+
}
|
|
250
376
|
const streamId = randomUUID().replace(/-/g, '');
|
|
251
377
|
const wasHiddenEmpty = !s.messages.length && !s.context_messages.length && !s.hasPendingPrompt;
|
|
252
378
|
const attachments = opts.attachments ?? [];
|
|
253
379
|
// A background wakeup stays one in a forked session too (TAL-460).
|
|
254
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);
|
|
255
387
|
s.workspace = opts.workspace;
|
|
256
388
|
s.model = opts.model;
|
|
257
389
|
s.model_provider = opts.modelProvider;
|
|
@@ -261,6 +393,12 @@ export class TurnRunner {
|
|
|
261
393
|
s.pending_attachments = attachments;
|
|
262
394
|
s.pending_started_at = this.deps.now();
|
|
263
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
|
+
}
|
|
264
402
|
if (['', 'Untitled', 'New Chat'].includes(str(s.title).trim())) {
|
|
265
403
|
const provisional = titleFrom([{ role: 'user', content: opts.msg }], s.title || 'Untitled');
|
|
266
404
|
if (provisional && !['', 'Untitled', 'New Chat'].includes(provisional.trim()))
|
|
@@ -268,19 +406,39 @@ export class TurnRunner {
|
|
|
268
406
|
}
|
|
269
407
|
if (this.deps.saveMode() === 'eager')
|
|
270
408
|
this.checkpointUserMessage(s, opts.msg, attachments, s.pending_started_at, source, streamId);
|
|
271
|
-
|
|
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
|
+
}
|
|
272
430
|
this.registry.writebackOwners.set(s.session_id, streamId);
|
|
273
431
|
// A new chat appears in the list; every turn tells open clients a run started (TAL-434).
|
|
274
432
|
this.deps.events.publish(wasHiddenEmpty ? 'session_new' : 'turn_started', { profile: s.profile, sessionId: s.session_id });
|
|
275
433
|
try {
|
|
276
|
-
this.deps.workspaces.setLastWorkspace(opts.workspace, s.profile);
|
|
434
|
+
(await this.deps.workspaces.setLastWorkspace(opts.workspace, s.profile));
|
|
277
435
|
}
|
|
278
436
|
catch { /* best effort */ }
|
|
279
437
|
const channel = this.registry.create(streamId, s.session_id);
|
|
438
|
+
admission.admitting = false;
|
|
280
439
|
if (opts.goalRelated)
|
|
281
440
|
this.registry.goalRelated.add(streamId);
|
|
282
|
-
this.
|
|
283
|
-
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) => {
|
|
284
442
|
this.deps.log(`[webui] ERROR turn ${streamId} crashed\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
285
443
|
}).finally(() => { this.workers.delete(streamId); });
|
|
286
444
|
this.workers.set(streamId, worker);
|
|
@@ -291,6 +449,10 @@ export class TurnRunner {
|
|
|
291
449
|
response.effective_model_provider = opts.modelProvider;
|
|
292
450
|
return response;
|
|
293
451
|
}
|
|
452
|
+
/** Resolves once the stream's worker has ended (settled, failed, or torn down); it never rejects. */
|
|
453
|
+
finished(streamId) {
|
|
454
|
+
return this.workers.get(streamId) ?? Promise.resolve();
|
|
455
|
+
}
|
|
294
456
|
/** Python `_checkpoint_user_message_for_eager_session_save`. */
|
|
295
457
|
checkpointUserMessage(s, msg, attachments, startedAt, source, turnId) {
|
|
296
458
|
const latest = s.messages[s.messages.length - 1];
|
|
@@ -299,20 +461,69 @@ export class TurnRunner {
|
|
|
299
461
|
s.messages.push(pendingUserRow(msg, attachments, startedAt, source, turnId));
|
|
300
462
|
}
|
|
301
463
|
// ── worker ───────────────────────────────────────────────────────────────
|
|
302
|
-
async run(sessionId, streamId, channel, opts) {
|
|
464
|
+
async run(sessionId, streamId, channel, opts, compactionDue) {
|
|
303
465
|
const { deps } = this;
|
|
304
466
|
let writer = null;
|
|
305
467
|
try {
|
|
306
|
-
writer = deps.journal.writer(sessionId, streamId);
|
|
468
|
+
writer = (await deps.journal.writer(sessionId, streamId));
|
|
307
469
|
this.writers.set(streamId, writer);
|
|
308
470
|
}
|
|
309
471
|
catch (error) {
|
|
310
472
|
deps.log(`[webui] WARNING: run journal degraded for stream ${streamId}: ${error.message}`);
|
|
311
473
|
}
|
|
312
|
-
const
|
|
474
|
+
const failure = { apperror: null, cancelled: false };
|
|
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;
|
|
520
|
+
if (event === 'apperror')
|
|
521
|
+
failure.apperror = data;
|
|
522
|
+
else if (event === 'cancel')
|
|
523
|
+
failure.cancelled = true;
|
|
313
524
|
for (const steerEvent of this.takeSteerEventsBefore(streamId, event))
|
|
314
|
-
put(steerEvent[0], steerEvent[1]);
|
|
315
|
-
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))
|
|
316
527
|
return;
|
|
317
528
|
// `cancel()` already wrote the terminal row and closed the stream: the worker's unwind adds no second one.
|
|
318
529
|
if (event === 'cancel' && !this.registry.streams.has(streamId)) {
|
|
@@ -325,7 +536,7 @@ export class TurnRunner {
|
|
|
325
536
|
let eventId = null;
|
|
326
537
|
if (writer) {
|
|
327
538
|
try {
|
|
328
|
-
eventId = writer.appendSseEvent(event, data, meta).event_id;
|
|
539
|
+
eventId = (await writer.appendSseEvent(event, data, meta)).event_id;
|
|
329
540
|
}
|
|
330
541
|
catch (error) {
|
|
331
542
|
this.registry.degradedJournals.add(streamId);
|
|
@@ -347,25 +558,43 @@ export class TurnRunner {
|
|
|
347
558
|
const activeRun = this.registry.activeRuns.get(streamId);
|
|
348
559
|
let s;
|
|
349
560
|
try {
|
|
350
|
-
s = deps.store.get(sessionId);
|
|
561
|
+
s = (await deps.store.get(sessionId));
|
|
351
562
|
}
|
|
352
563
|
catch {
|
|
353
|
-
put('apperror', { type: 'error', terminal_state: 'error', message: 'Session not found', session_id: sessionId });
|
|
354
|
-
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);
|
|
355
566
|
return;
|
|
356
567
|
}
|
|
357
568
|
const msgText = opts.msg;
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
};
|
|
366
582
|
// Before the first await: a Stop can land at any point after admission.
|
|
367
|
-
|
|
368
|
-
|
|
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
|
+
}
|
|
369
598
|
const activeTurnToken = buildActiveTurnToken(streamId, s.pending_started_at);
|
|
370
599
|
const sidecar = deps.sidecar();
|
|
371
600
|
const partialText = this.registry.partialText.get(streamId) ?? [];
|
|
@@ -382,21 +611,19 @@ export class TurnRunner {
|
|
|
382
611
|
// TAL-397: the live meter. Counters start from the session's and follow the Agent's; the rate counts text deltas.
|
|
383
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 };
|
|
384
613
|
const meter = { deltas: 0, first: 0, last: 0, sentAt: null };
|
|
385
|
-
const putMetering = (force) => {
|
|
386
|
-
const now = deps.now();
|
|
614
|
+
const putMetering = async (force, now = deps.now()) => {
|
|
387
615
|
if (!force && meter.sentAt !== null && now - meter.sentAt < METERING_INTERVAL_S)
|
|
388
616
|
return;
|
|
389
617
|
meter.sentAt = now;
|
|
390
618
|
const tps = meter.deltas && meter.last > meter.first ? Math.round((meter.deltas / (meter.last - meter.first)) * 10) / 10 : null;
|
|
391
|
-
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 });
|
|
392
620
|
};
|
|
393
|
-
const meterDelta = () => {
|
|
394
|
-
const now = deps.now();
|
|
621
|
+
const meterDelta = async (now = deps.now()) => {
|
|
395
622
|
if (!meter.deltas)
|
|
396
623
|
meter.first = now;
|
|
397
624
|
meter.last = now;
|
|
398
625
|
meter.deltas += 1;
|
|
399
|
-
putMetering(false);
|
|
626
|
+
await putMetering(false, now);
|
|
400
627
|
};
|
|
401
628
|
// TAL-460: a background turn's text is held back while it could still be a silence marker, so one never flashes by.
|
|
402
629
|
// ponytail: checks the turn's whole text, so a marker after earlier prose streams until the settled row hides it;
|
|
@@ -405,13 +632,11 @@ export class TurnRunner {
|
|
|
405
632
|
let heldText = '';
|
|
406
633
|
const titles = new ReasoningTitleTracker();
|
|
407
634
|
let capturedTerminalError = null;
|
|
408
|
-
const controller = new AbortController();
|
|
409
|
-
this.abortControllers.set(streamId, controller);
|
|
410
635
|
const attachments = opts.attachments ?? [];
|
|
411
636
|
// TAL-545: one image mode decides both this turn's upload and the replayed history, which a text-mode turn sends
|
|
412
637
|
// without native images; only a turn with an image on either side asks for it.
|
|
413
638
|
const imageMode = attachments.some(isImageAttachment) || hasNativeImages(apiHistory) ? await this.imageInputMode(sessionId, s, opts, controller.signal) : null;
|
|
414
|
-
const userMessage = this.buildUserMessage(
|
|
639
|
+
const userMessage = this.buildUserMessage(opts.prompt ?? msgText, attachments, opts.workspace, sessionId, imageMode);
|
|
415
640
|
const conversationHistory = imageMode === 'text' ? withoutNativeImages(apiHistory) : apiHistory;
|
|
416
641
|
// The prompt the Agent actually gets, native image parts included.
|
|
417
642
|
const stop = this.stopContexts.get(streamId);
|
|
@@ -420,15 +645,25 @@ export class TurnRunner {
|
|
|
420
645
|
if (activeRun)
|
|
421
646
|
activeRun.phase = 'running';
|
|
422
647
|
const settledAt = { value: false };
|
|
423
|
-
let
|
|
648
|
+
let succeeded = false;
|
|
649
|
+
let ownershipRefused = false;
|
|
650
|
+
let agentDispatched = false;
|
|
651
|
+
let streamWrites = Promise.resolve();
|
|
652
|
+
let streamWriteError;
|
|
424
653
|
// TAL-424: steers an errored turn ended without taking, sent as one follow-up turn once this one is torn down.
|
|
425
654
|
let followUp = [];
|
|
426
655
|
try {
|
|
427
656
|
if (!sidecar)
|
|
428
657
|
throw new SidecarError('The Agent sidecar is not running; chat is unavailable until it starts.', { condition: 'sidecar_unavailable' });
|
|
429
658
|
// Python: budgets, reasoning config, personality and delivery context come from the profile's config.yaml; the
|
|
430
|
-
// system message carries the frozen session workspace, the WebUI guidance rides as the ephemeral prompt
|
|
659
|
+
// system message carries the frozen session workspace, the WebUI guidance rides as the ephemeral prompt with this
|
|
660
|
+
// turn's workspace (TAL-710: never a tag on the user's message, which the Agent can persist from memory).
|
|
431
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
|
+
: [];
|
|
432
667
|
// The sidecar's per-session YOLO state does not survive its restarts: push the local flag (either way) before
|
|
433
668
|
// the turn so a stale sidecar-side enable cannot auto-approve a session the UI reports as guarded.
|
|
434
669
|
await this.withYoloLock(sessionId, async () => { try {
|
|
@@ -438,154 +673,226 @@ export class TurnRunner {
|
|
|
438
673
|
// Last yield before `chat.start`: a cancel that landed during the attachment, config or YOLO awaits must not
|
|
439
674
|
// start an Agent turn (the client also refuses an already-aborted signal outright).
|
|
440
675
|
if (this.registry.cancelled.has(streamId)) {
|
|
441
|
-
this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
442
|
-
put('cancel', this.cancelFrame(sessionId));
|
|
676
|
+
await this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
677
|
+
await put('cancel', (await this.cancelFrame(sessionId)));
|
|
443
678
|
return;
|
|
444
679
|
}
|
|
445
|
-
const frozenWorkspace = str(s.created_workspace) || str(s.workspace);
|
|
446
680
|
const turnContext = {
|
|
447
|
-
system_message: workspaceSystemMessage(
|
|
448
|
-
ephemeral_system_prompt: webuiEphemeralSystemPrompt({ config: cfg,
|
|
449
|
-
|
|
450
|
-
max_tokens: maxTokensFromConfig(cfg),
|
|
451
|
-
reasoning_config: reasoningConfigFromConfig(cfg, opts.modelProvider ?? s.model_provider ?? null),
|
|
681
|
+
system_message: workspaceSystemMessage(str(s.created_workspace) || str(s.workspace)),
|
|
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 ?? {}),
|
|
452
684
|
};
|
|
453
685
|
// Python `_public_prefill_context_status`: the session-recall prefill hook was dropped with TAL-245, so the
|
|
454
686
|
// context frame always reports the not-configured shape.
|
|
455
687
|
const persistentBefore = persistentStateSnapshot(deps.profileHome(s.profile));
|
|
456
|
-
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;
|
|
457
692
|
const result = await sidecar.call('chat.start', {
|
|
458
|
-
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,
|
|
459
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 } : {}),
|
|
460
698
|
}, {
|
|
461
699
|
signal: controller.signal,
|
|
462
700
|
timeoutMs: 0,
|
|
463
701
|
onStream: (frame) => {
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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);
|
|
477
724
|
return;
|
|
478
725
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
reasoningText.push(str(data.text));
|
|
488
|
-
// Python `reasoning_event_payload`: bold or first-line titles ride on the delta they complete.
|
|
489
|
-
put('reasoning', titles.prepare(reasoningEventPayload(str(data.text), reasoningText.join(''))));
|
|
490
|
-
meterDelta();
|
|
491
|
-
return;
|
|
492
|
-
}
|
|
493
|
-
case 'usage': {
|
|
494
|
-
const usage = ChatUsageSchema.safeParse(data);
|
|
495
|
-
if (usage.success) {
|
|
496
|
-
applyAgentUsage(liveUsage, usage.data);
|
|
497
|
-
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;
|
|
498
734
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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);
|
|
506
742
|
return;
|
|
507
743
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
519
|
-
case 'tool_complete': {
|
|
520
|
-
// TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
|
|
521
|
-
// are its display sections, which keep the stderr and exit code the flat preview drops.
|
|
522
|
-
const { raw_result: rawResult, todo_result: todoResult, result_diff: resultDiff, ...complete } = data;
|
|
523
|
-
const outcome = toolOutcome(rawResult);
|
|
524
|
-
complete.is_error = outcome.is_error;
|
|
525
|
-
if (rawResult !== undefined)
|
|
526
|
-
complete.result_view = outcome.result_view;
|
|
527
|
-
// TAL-448: a file edit's change, counted over the whole diff the sidecar forwards; redacted with the frame.
|
|
528
|
-
const editDiff = toolEditDiff(data.name, { diff: resultDiff });
|
|
529
|
-
if (editDiff)
|
|
530
|
-
complete.edit_diff = editDiff;
|
|
531
|
-
const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
|
|
532
|
-
if (tc) {
|
|
533
|
-
const startedAt = toolStartedAt.get(tc);
|
|
534
|
-
if (startedAt !== undefined)
|
|
535
|
-
complete.duration = Math.round(Math.max(0, deps.now() - startedAt) * 1000) / 1000;
|
|
536
|
-
// TAL-315: the decided view too, so a failed or cancelled turn's snapshot keeps the sections shown live.
|
|
537
|
-
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;
|
|
538
754
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
this.liveTodos.set(streamId, todos);
|
|
548
|
-
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;
|
|
549
763
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
+
}
|
|
562
797
|
return;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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);
|
|
579
889
|
}
|
|
580
|
-
|
|
581
|
-
if (data.kind === 'terminal_error' && !capturedTerminalError)
|
|
582
|
-
capturedTerminalError = str(data.message);
|
|
583
|
-
return;
|
|
584
|
-
default:
|
|
585
|
-
put(frame.event, data);
|
|
586
|
-
}
|
|
890
|
+
}).catch((error) => { streamWriteError = error instanceof Error ? error : new Error(String(error)); });
|
|
587
891
|
},
|
|
588
892
|
});
|
|
893
|
+
await streamWrites;
|
|
894
|
+
if (streamWriteError !== undefined)
|
|
895
|
+
throw streamWriteError;
|
|
589
896
|
settledAt.value = true;
|
|
590
897
|
// TAL-545: every transcript the Agent hands back settles with the history's images, not the request's projection.
|
|
591
898
|
const withImages = (rows) => withNativeImagesRestored(rows, conversationHistory, apiHistory);
|
|
@@ -598,14 +905,14 @@ export class TurnRunner {
|
|
|
598
905
|
const interrupted = await this.interrupts.get(streamId);
|
|
599
906
|
applyAgentUsage(liveUsage, result.usage);
|
|
600
907
|
this.liveUsage.set(streamId, liveUsage);
|
|
601
|
-
this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ? withImages(interrupted.checkpoint) : result.messages);
|
|
602
|
-
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)));
|
|
603
910
|
return;
|
|
604
911
|
}
|
|
605
912
|
// Writeback only while this stream still owns the session (Python `_stream_writeback_is_current`).
|
|
606
913
|
let current;
|
|
607
914
|
try {
|
|
608
|
-
current = deps.store.get(sessionId);
|
|
915
|
+
current = (await deps.store.get(sessionId));
|
|
609
916
|
}
|
|
610
917
|
catch {
|
|
611
918
|
current = s;
|
|
@@ -641,16 +948,15 @@ export class TurnRunner {
|
|
|
641
948
|
await this.steerRewrites.get(streamId);
|
|
642
949
|
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, str(result.pending_steer), 'followup');
|
|
643
950
|
followUp = leftovers;
|
|
644
|
-
this.persistError(s, streamId, classification.label, payload, activeTurnToken, resultMessages);
|
|
951
|
+
await this.persistError(s, streamId, classification.label, payload, activeTurnToken, resultMessages, result.provider);
|
|
645
952
|
// TAL-512: a btw error carries no session: its journaled frame must not keep a copy of the parent conversation.
|
|
646
953
|
if (!opts.ephemeral)
|
|
647
|
-
payload.session =
|
|
954
|
+
payload.session = (await this.publicTerminalSession(s));
|
|
648
955
|
payload.session_id = s.session_id;
|
|
649
956
|
payload.old_session_id = sessionId;
|
|
650
957
|
for (const [event, data] of steerEvents)
|
|
651
|
-
put(event, data);
|
|
652
|
-
put('apperror', payload);
|
|
653
|
-
failed = true;
|
|
958
|
+
(await put(event, data));
|
|
959
|
+
await put('apperror', payload);
|
|
654
960
|
return;
|
|
655
961
|
}
|
|
656
962
|
// TAL-512: a btw turn passes the failure check above first, so its last assistant row is this turn's reply.
|
|
@@ -663,11 +969,12 @@ export class TurnRunner {
|
|
|
663
969
|
break;
|
|
664
970
|
}
|
|
665
971
|
}
|
|
666
|
-
|
|
972
|
+
succeeded = true;
|
|
973
|
+
await opts.onDone?.(answer);
|
|
667
974
|
// Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
|
|
668
|
-
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' });
|
|
669
976
|
try {
|
|
670
|
-
|
|
977
|
+
await deps.store.delete(sessionId, { tombstone: false });
|
|
671
978
|
}
|
|
672
979
|
catch { /* ignore */ }
|
|
673
980
|
deps.store.sessions.delete(sessionId);
|
|
@@ -694,8 +1001,9 @@ export class TurnRunner {
|
|
|
694
1001
|
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, result.pending_steer, 'followup');
|
|
695
1002
|
s.messages = withoutToolImages(mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] }));
|
|
696
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;
|
|
697
1006
|
if (result.compressed) {
|
|
698
|
-
s.post_compression_context_tokens_estimate = estimate;
|
|
699
1007
|
// TAL-540: the anchor is the last visible row before this turn (never its eager checkpoint); the merged transcript
|
|
700
1008
|
// holds no compaction marker, so the summary is the model context's newest one.
|
|
701
1009
|
const before = visibleMessagesForAnchor(previousMessages.filter((m) => m._turn_id !== streamId));
|
|
@@ -720,20 +1028,13 @@ export class TurnRunner {
|
|
|
720
1028
|
s.threshold_tokens = result.context.threshold_tokens;
|
|
721
1029
|
if (typeof result.context.last_prompt_tokens === 'number')
|
|
722
1030
|
s.last_prompt_tokens = result.context.last_prompt_tokens;
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
...meteringUsage(s), context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
727
|
-
post_compression_context_tokens_estimate: s.post_compression_context_tokens_estimate, ...contextUsage(s, () => deps.service().deps.contextLengthFor(s.model, s.model_provider)),
|
|
728
|
-
};
|
|
729
|
-
put('compressed', { session_id: sessionId, old_session_id: sessionId, new_session_id: sessionId, continuation_session_id: sessionId, message: 'Compression finished', usage });
|
|
730
|
-
}
|
|
1031
|
+
s.next_turn_threshold_tokens = result.context.next_turn_threshold_tokens ?? null;
|
|
1032
|
+
if (result.compressed)
|
|
1033
|
+
(await put('compressed', this.compressedFrame(s)));
|
|
731
1034
|
s.tool_calls = extractToolCallsFromMessages(s.messages, liveToolCalls, s.tool_calls);
|
|
732
|
-
s
|
|
733
|
-
|
|
734
|
-
s.
|
|
735
|
-
s.pending_started_at = null;
|
|
736
|
-
s.pending_user_source = null;
|
|
1035
|
+
this.clearPendingTurn(s);
|
|
1036
|
+
// TAL-576: a turn that ran means the provider has usable credentials again.
|
|
1037
|
+
s.process_wakeup_pause = null;
|
|
737
1038
|
const attachments = opts.attachments ?? [];
|
|
738
1039
|
if (attachments.length) {
|
|
739
1040
|
for (let i = s.messages.length - 1; i >= 0; i -= 1) {
|
|
@@ -786,8 +1087,9 @@ export class TurnRunner {
|
|
|
786
1087
|
this.persistConsumedSteers(s, streamId, previousStartedAt(s, activeRun), now);
|
|
787
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
|
|
788
1089
|
// (all of them, the dropped summary request too).
|
|
789
|
-
|
|
790
|
-
|
|
1090
|
+
this.settleStateDb(s, streamId, agentRows, { previousContext });
|
|
1091
|
+
recordBackgroundReview(s, streamId);
|
|
1092
|
+
await deps.store.save(s);
|
|
791
1093
|
deps.pending.clearApprovals(sessionId);
|
|
792
1094
|
deps.pending.clearClarifies(sessionId);
|
|
793
1095
|
deps.events.publish('session_done', { profile: s.profile, sessionId: s.session_id });
|
|
@@ -795,9 +1097,9 @@ export class TurnRunner {
|
|
|
795
1097
|
try {
|
|
796
1098
|
const changes = persistentStateChanges(persistentBefore, persistentStateSnapshot(deps.profileHome(s.profile)));
|
|
797
1099
|
if (changes.memory_saved)
|
|
798
|
-
put('state_saved', { session_id: sessionId, kind: 'memory', action: 'saved' });
|
|
1100
|
+
(await put('state_saved', { session_id: sessionId, kind: 'memory', action: 'saved' }));
|
|
799
1101
|
for (const change of changes.skills)
|
|
800
|
-
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 }));
|
|
801
1103
|
}
|
|
802
1104
|
catch (error) {
|
|
803
1105
|
deps.log(`[webui] persistent state change detection failed for ${sessionId}: ${error.message}`);
|
|
@@ -813,7 +1115,7 @@ export class TurnRunner {
|
|
|
813
1115
|
cache_hit_percent: cacheHit(cacheReadTokens, inputTokens), turn_cache_hit_percent: cacheHit(turnCacheRead, turnInput), duration_seconds: Math.round(duration * 1000) / 1000,
|
|
814
1116
|
context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
815
1117
|
};
|
|
816
|
-
const doneSession =
|
|
1118
|
+
const doneSession = (await this.publicTerminalSession(s));
|
|
817
1119
|
// TAL-299: the ring's figures match the terminal session's (and a detail reload's).
|
|
818
1120
|
for (const key of CONTEXT_USAGE_FIELDS)
|
|
819
1121
|
doneUsage[key] = doneSession[key];
|
|
@@ -832,59 +1134,76 @@ export class TurnRunner {
|
|
|
832
1134
|
if (result.tool_limit_reached)
|
|
833
1135
|
donePayload.terminal_reason = 'max_iterations';
|
|
834
1136
|
// TAL-397: the live meter ends on the persisted counters and turn rate.
|
|
835
|
-
put('metering', { session_id: sessionId, usage: meteringUsage(s), tps: doneUsage.tps ?? null, tps_available: doneUsage.tps !== undefined, estimated: false });
|
|
836
|
-
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);
|
|
837
1139
|
for (const [event, payload] of steerEvents)
|
|
838
|
-
put(event, payload);
|
|
1140
|
+
(await put(event, payload));
|
|
839
1141
|
// The turn is over: release admission before the title work so a follow-up message is accepted while the
|
|
840
1142
|
// (up to two) title prompts run; the channel and journal stay open for the `title` events (Python retired the
|
|
841
1143
|
// worker before its daemon-thread title generation).
|
|
842
1144
|
this.registry.activeRuns.delete(streamId);
|
|
843
1145
|
this.settledStreams.add(streamId);
|
|
844
|
-
|
|
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);
|
|
845
1150
|
// Before the title work, so a generated title's own state.db sync lands after this one's provisional title.
|
|
846
1151
|
await deps.syncUsage?.(s, usedModel || null);
|
|
847
1152
|
// Title work and the goal judge run side by side; `stream_end` follows both, so their frames reach the stream.
|
|
848
1153
|
const work = await Promise.allSettled([this.backgroundTitle(s, put), this.continueGoal(s, streamId, put)]);
|
|
849
|
-
put('stream_end', { session_id: sessionId });
|
|
1154
|
+
await put('stream_end', { session_id: sessionId });
|
|
850
1155
|
for (const r of work)
|
|
851
1156
|
if (r.status === 'rejected')
|
|
852
1157
|
throw r.reason;
|
|
853
|
-
|
|
854
|
-
opts.onDone?.(lastAnswer(s.messages)
|
|
1158
|
+
succeeded = true;
|
|
1159
|
+
await opts.onDone?.(lastAnswer(s.messages));
|
|
855
1160
|
}
|
|
856
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
|
+
}
|
|
857
1177
|
if (settledAt.value && !(error instanceof SidecarError)) {
|
|
858
1178
|
deps.log(`[webui] ERROR settling turn ${streamId}\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
859
1179
|
}
|
|
860
|
-
failed = true;
|
|
861
1180
|
if (this.registry.cancelled.has(streamId)) {
|
|
862
|
-
this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
863
|
-
put('cancel', this.cancelFrame(sessionId));
|
|
1181
|
+
await this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
1182
|
+
await put('cancel', (await this.cancelFrame(sessionId)));
|
|
864
1183
|
return;
|
|
865
1184
|
}
|
|
866
1185
|
const message = error instanceof Error ? error.message : String(error);
|
|
867
1186
|
const condition = error instanceof SidecarError ? error.condition : undefined;
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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);
|
|
874
1193
|
try {
|
|
875
1194
|
let current = s;
|
|
876
1195
|
try {
|
|
877
|
-
current = deps.store.get(sessionId);
|
|
1196
|
+
current = (await deps.store.get(sessionId));
|
|
878
1197
|
}
|
|
879
1198
|
catch {
|
|
880
1199
|
current = s;
|
|
881
1200
|
}
|
|
882
1201
|
if (current.active_stream_id === streamId) {
|
|
883
1202
|
for (const [event, data] of this.takeSteerEventsBefore(streamId, 'apperror'))
|
|
884
|
-
put(event, data);
|
|
885
|
-
this.persistError(current, streamId, classification.label, payload, activeTurnToken);
|
|
1203
|
+
(await put(event, data));
|
|
1204
|
+
await this.persistError(current, streamId, classification.label, payload, activeTurnToken);
|
|
886
1205
|
if (!opts.ephemeral)
|
|
887
|
-
payload.session =
|
|
1206
|
+
payload.session = (await this.publicTerminalSession(current));
|
|
888
1207
|
}
|
|
889
1208
|
}
|
|
890
1209
|
catch (persistError) {
|
|
@@ -892,15 +1211,20 @@ export class TurnRunner {
|
|
|
892
1211
|
}
|
|
893
1212
|
payload.session_id = sessionId;
|
|
894
1213
|
payload.old_session_id = sessionId;
|
|
895
|
-
put('apperror', payload);
|
|
1214
|
+
await put('apperror', payload);
|
|
896
1215
|
}
|
|
897
1216
|
finally {
|
|
898
|
-
this.teardown(sessionId, streamId);
|
|
1217
|
+
await this.teardown(sessionId, streamId);
|
|
899
1218
|
// After persistence and teardown: a background route's failure cleanup deletes the hidden session, which must
|
|
900
1219
|
// not race the error writeback above (a re-saved session would resurface in the sidebar).
|
|
901
|
-
if (
|
|
902
|
-
opts.onFailed?.();
|
|
903
|
-
|
|
1220
|
+
if (!succeeded)
|
|
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);
|
|
904
1228
|
}
|
|
905
1229
|
}
|
|
906
1230
|
/**
|
|
@@ -941,9 +1265,8 @@ export class TurnRunner {
|
|
|
941
1265
|
* after the text (TAL-276), so an attachment-only turn still gives the model a request and its transcript row a
|
|
942
1266
|
* distinct identity.
|
|
943
1267
|
*/
|
|
944
|
-
buildUserMessage(
|
|
945
|
-
const
|
|
946
|
-
const withFiles = () => attachedFilesPrompt(text, attachments);
|
|
1268
|
+
buildUserMessage(msgText, attachments, workspace, sessionId, imageMode) {
|
|
1269
|
+
const withFiles = () => attachedFilesPrompt(msgText, attachments);
|
|
947
1270
|
const candidates = attachments.filter(isImageAttachment);
|
|
948
1271
|
if (!candidates.length || imageMode !== 'native')
|
|
949
1272
|
return withFiles();
|
|
@@ -981,18 +1304,23 @@ export class TurnRunner {
|
|
|
981
1304
|
}
|
|
982
1305
|
return images ? [{ type: 'text', text: withFiles() }, ...parts] : withFiles();
|
|
983
1306
|
}
|
|
1307
|
+
/** The settled session as a terminal frame carries it: the display window, redacted. */
|
|
1308
|
+
async publicTerminalSession(s) {
|
|
1309
|
+
return redactSessionData((await this.terminalSessionPayload(s)), this.deps.redactEnabled());
|
|
1310
|
+
}
|
|
984
1311
|
/**
|
|
985
1312
|
* Python `_session_payload_with_terminal_window`: the same visible-message window contract as `GET /api/session?
|
|
986
1313
|
* msg_limit=` (renderable rows, limited payload shape, windowed tool calls, todo state) with the full count.
|
|
987
1314
|
*/
|
|
988
|
-
terminalSessionPayload(s) {
|
|
989
|
-
const payload =
|
|
990
|
-
payload.
|
|
991
|
-
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);
|
|
992
1320
|
const service = this.deps.service();
|
|
993
|
-
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 })));
|
|
994
1322
|
const [window, offset] = messageWindowForDisplay(scened, TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT, null);
|
|
995
|
-
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));
|
|
996
1324
|
payload.messages = limited;
|
|
997
1325
|
payload.message_count = s.messages.length;
|
|
998
1326
|
payload._messages_offset = offset;
|
|
@@ -1002,16 +1330,43 @@ export class TurnRunner {
|
|
|
1002
1330
|
payload.compression_reference = compressionReference(s, scened);
|
|
1003
1331
|
return payload;
|
|
1004
1332
|
}
|
|
1005
|
-
/** Python `
|
|
1006
|
-
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
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) {
|
|
1010
1344
|
s.active_stream_id = null;
|
|
1011
1345
|
s.pending_user_message = null;
|
|
1012
1346
|
s.pending_attachments = [];
|
|
1013
1347
|
s.pending_started_at = null;
|
|
1014
1348
|
s.pending_user_source = null;
|
|
1349
|
+
}
|
|
1350
|
+
/** Python `_materialize_pending_user_turn_before_error` + error message append + save. */
|
|
1351
|
+
/** `runtimeProvider`: the provider the Agent resolved for the turn, when it reported one. */
|
|
1352
|
+
async persistError(s, streamId, label, payload, activeTurnToken, agentRows = [], runtimeProvider = '') {
|
|
1353
|
+
const startedAt = s.pending_started_at;
|
|
1354
|
+
// TAL-576: an automatic wakeup with no usable credentials pauses the next ones instead of failing each.
|
|
1355
|
+
if (s.pending_user_source === 'process_wakeup' && payload.type === 'credential_pool_empty') {
|
|
1356
|
+
// A profile whose home now escapes the profiles root (`profileHome` throws) records no pause; the error still persists.
|
|
1357
|
+
let home = null;
|
|
1358
|
+
try {
|
|
1359
|
+
home = this.deps.profileHome(s.profile);
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
home = null;
|
|
1363
|
+
}
|
|
1364
|
+
if (home !== null)
|
|
1365
|
+
recordWakeupPause(s, home, this.deps.now(), runtimeProvider, this.deps.configPath?.(home));
|
|
1366
|
+
}
|
|
1367
|
+
this.materializePendingUserTurn(s, activeTurnToken, streamId);
|
|
1368
|
+
const duration = typeof startedAt === 'number' && startedAt > 0 ? Math.max(0, this.deps.now() - startedAt) : null;
|
|
1369
|
+
this.clearPendingTurn(s);
|
|
1015
1370
|
this.appendPartialSnapshot(s, streamId);
|
|
1016
1371
|
const hint = str(payload.hint);
|
|
1017
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 };
|
|
@@ -1035,10 +1390,15 @@ export class TurnRunner {
|
|
|
1035
1390
|
s.messages.push(errorMessage);
|
|
1036
1391
|
this.keepLiveState(s, streamId);
|
|
1037
1392
|
this.persistConsumedSteers(s, streamId, startedAt, this.deps.now());
|
|
1038
|
-
|
|
1039
|
-
|
|
1393
|
+
// TAL-921: the next prompt remembers the turn's work; an exhausted compression's work would only overflow again. One
|
|
1394
|
+
// read builds the context and settles, so a row committed in between is never covered without reaching either.
|
|
1395
|
+
const read = this.deps.service().stateDbRead(s);
|
|
1396
|
+
const context = payload.type === 'compression_exhausted' ? null : this.unsettledContext(streamId, agentRows, read);
|
|
1397
|
+
if (context)
|
|
1398
|
+
s.context_messages = context;
|
|
1399
|
+
this.settleStateDb(s, streamId, agentRows, { read });
|
|
1040
1400
|
try {
|
|
1041
|
-
this.deps.store.save(s);
|
|
1401
|
+
(await this.deps.store.save(s));
|
|
1042
1402
|
}
|
|
1043
1403
|
catch (error) {
|
|
1044
1404
|
this.deps.log(`[webui] WARNING: failed to save error turn for ${s.session_id}: ${error.message}`);
|
|
@@ -1049,6 +1409,24 @@ export class TurnRunner {
|
|
|
1049
1409
|
this.deps.pending.clearClarifies(s.session_id);
|
|
1050
1410
|
this.deps.events.publish('session_error', { profile: s.profile, sessionId: s.session_id });
|
|
1051
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
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* TAL-921: the model context a turn that ends without its settled result leaves: the work its Agent reported, else the
|
|
1420
|
+
* rows it committed past its starting read in `read`, the read its settlement uses.
|
|
1421
|
+
*/
|
|
1422
|
+
unsettledContext(streamId, agentRows, read) {
|
|
1423
|
+
const stop = this.stopContexts.get(streamId);
|
|
1424
|
+
if (!stop)
|
|
1425
|
+
return null;
|
|
1426
|
+
const after = stop.stateDbStartGeneration !== null && read.generation !== null && stop.stateDbStartGeneration !== read.generation ? -1 : stop.stateDbStartId;
|
|
1427
|
+
const stateRows = after === null ? [] : read.rows.filter((m) => typeof m._state_db_row_id === 'number' && m._state_db_row_id > after);
|
|
1428
|
+
return this.deps.service().unsettledTurnContext({ ...stop, streamedText: (this.registry.partialText.get(streamId) ?? []).join(''), agentRows, stateRows });
|
|
1429
|
+
}
|
|
1052
1430
|
materializePendingUserTurn(s, activeTurnToken, turnId) {
|
|
1053
1431
|
const pendingText = str(s.pending_user_message);
|
|
1054
1432
|
if (!s.hasPendingPrompt)
|
|
@@ -1094,11 +1472,11 @@ export class TurnRunner {
|
|
|
1094
1472
|
* Python `_emit_cancel_event`: the terminal frame carries the settled session so tabs can render the partial without a
|
|
1095
1473
|
* reload. TAL-364: no copy — clients show their one localized status for the `cancelled` outcome.
|
|
1096
1474
|
*/
|
|
1097
|
-
cancelFrame(sessionId) {
|
|
1475
|
+
async cancelFrame(sessionId) {
|
|
1098
1476
|
let snapshot = null;
|
|
1099
1477
|
if (sessionId)
|
|
1100
1478
|
try {
|
|
1101
|
-
snapshot =
|
|
1479
|
+
snapshot = (await this.publicTerminalSession((await this.deps.store.get(sessionId))));
|
|
1102
1480
|
}
|
|
1103
1481
|
catch {
|
|
1104
1482
|
snapshot = null;
|
|
@@ -1110,12 +1488,12 @@ export class TurnRunner {
|
|
|
1110
1488
|
* turn's model context keeps what the Agent had captured (`checkpoint`, its canonical transcript) and the prose that
|
|
1111
1489
|
* streamed, so the next request continues from it; the terminal row carries the outcome and no copy.
|
|
1112
1490
|
*/
|
|
1113
|
-
finalizeCancelled(s, streamId, ephemeral, checkpoint = null) {
|
|
1491
|
+
async finalizeCancelled(s, streamId, ephemeral, checkpoint = null) {
|
|
1114
1492
|
if (this.registry.writebackOwners.get(s.session_id) !== streamId)
|
|
1115
1493
|
return false;
|
|
1116
1494
|
let current = s;
|
|
1117
1495
|
try {
|
|
1118
|
-
current = this.deps.store.get(s.session_id);
|
|
1496
|
+
current = (await this.deps.store.get(s.session_id));
|
|
1119
1497
|
}
|
|
1120
1498
|
catch {
|
|
1121
1499
|
return false;
|
|
@@ -1124,7 +1502,7 @@ export class TurnRunner {
|
|
|
1124
1502
|
return false;
|
|
1125
1503
|
if (ephemeral) {
|
|
1126
1504
|
try {
|
|
1127
|
-
|
|
1505
|
+
await this.deps.store.delete(current.session_id, { tombstone: false });
|
|
1128
1506
|
}
|
|
1129
1507
|
catch { /* ignore */ }
|
|
1130
1508
|
this.deps.store.sessions.delete(current.session_id);
|
|
@@ -1136,9 +1514,9 @@ export class TurnRunner {
|
|
|
1136
1514
|
if (quiet && current.active_stream_id === null) {
|
|
1137
1515
|
// TAL-493: rows its worker committed while unwinding are covered by what it reports; others are kept.
|
|
1138
1516
|
if (checkpoint) {
|
|
1139
|
-
this.
|
|
1517
|
+
this.settleStateDb(current, streamId, checkpoint);
|
|
1140
1518
|
try {
|
|
1141
|
-
this.deps.store.save(current);
|
|
1519
|
+
(await this.deps.store.save(current));
|
|
1142
1520
|
}
|
|
1143
1521
|
catch {
|
|
1144
1522
|
return false;
|
|
@@ -1148,26 +1526,28 @@ export class TurnRunner {
|
|
|
1148
1526
|
}
|
|
1149
1527
|
const stop = this.stopContexts.get(streamId);
|
|
1150
1528
|
const canonical = stop !== undefined && checkpoint !== null && checkpointTurnStart(checkpoint, stop.msgText, stop.historyLength) !== null;
|
|
1151
|
-
|
|
1529
|
+
// TAL-921: one state.db read builds the context and settles the turn.
|
|
1530
|
+
const settle = (read) => {
|
|
1152
1531
|
if (!stop)
|
|
1153
1532
|
return null;
|
|
1154
1533
|
if (canonical)
|
|
1155
1534
|
stop.checkpointed = true;
|
|
1156
|
-
return
|
|
1535
|
+
return this.unsettledContext(streamId, checkpoint, read);
|
|
1157
1536
|
};
|
|
1158
1537
|
if (current.messages.some((m) => isCancelMarker(m)) && current.active_stream_id === null && !current.pending_user_message) {
|
|
1159
1538
|
// The worker's canonical result can arrive after cancel() settled without a checkpoint (the interrupt reply failed
|
|
1160
1539
|
// or timed out): it replaces this stream's context and never adds a second Stop row. A settlement that already had
|
|
1161
1540
|
// one keeps it, so work the Agent finished after the Stop stays out.
|
|
1162
1541
|
const ownStop = current.messages.some((m) => isCancelMarker(m) && m._turn_id === streamId);
|
|
1163
|
-
const late = canonical && !stop.checkpointed && ownStop ? settle() : null;
|
|
1164
|
-
if (late)
|
|
1165
|
-
current.context_messages = dedupeContext(late);
|
|
1166
1542
|
// TAL-493: the worker's rows committed after the first settlement are covered by what it reports; others are kept.
|
|
1167
|
-
if (
|
|
1168
|
-
this.deps.service().
|
|
1543
|
+
if (ownStop && checkpoint) {
|
|
1544
|
+
const read = this.deps.service().stateDbRead(current);
|
|
1545
|
+
const late = canonical && !stop.checkpointed ? settle(read) : null;
|
|
1546
|
+
if (late)
|
|
1547
|
+
current.context_messages = dedupeContext(late);
|
|
1548
|
+
this.settleStateDb(current, streamId, checkpoint, { read });
|
|
1169
1549
|
try {
|
|
1170
|
-
this.deps.store.save(current);
|
|
1550
|
+
(await this.deps.store.save(current));
|
|
1171
1551
|
}
|
|
1172
1552
|
catch {
|
|
1173
1553
|
return false;
|
|
@@ -1177,23 +1557,20 @@ export class TurnRunner {
|
|
|
1177
1557
|
}
|
|
1178
1558
|
const startedAt = current.pending_started_at;
|
|
1179
1559
|
this.materializePendingUserTurn(current, buildActiveTurnToken(streamId, current.pending_started_at), streamId);
|
|
1180
|
-
const
|
|
1560
|
+
const read = this.deps.service().stateDbRead(current);
|
|
1561
|
+
const context = settle(read);
|
|
1181
1562
|
if (context)
|
|
1182
1563
|
current.context_messages = dedupeContext(context);
|
|
1183
|
-
current
|
|
1184
|
-
current.pending_user_message = null;
|
|
1185
|
-
current.pending_attachments = [];
|
|
1186
|
-
current.pending_started_at = null;
|
|
1187
|
-
current.pending_user_source = null;
|
|
1564
|
+
this.clearPendingTurn(current);
|
|
1188
1565
|
if (!quiet) {
|
|
1189
1566
|
this.appendPartialSnapshot(current, streamId);
|
|
1190
1567
|
current.messages.push({ role: 'assistant', content: '', _error: true, _terminal_state: 'cancelled', timestamp: Math.trunc(this.deps.now()), _turn_id: streamId });
|
|
1191
1568
|
}
|
|
1192
1569
|
this.keepLiveState(current, streamId);
|
|
1193
1570
|
this.persistConsumedSteers(current, streamId, startedAt, this.deps.now());
|
|
1194
|
-
this.
|
|
1571
|
+
this.settleStateDb(current, streamId, checkpoint, { read });
|
|
1195
1572
|
try {
|
|
1196
|
-
this.deps.store.save(current);
|
|
1573
|
+
(await this.deps.store.save(current));
|
|
1197
1574
|
}
|
|
1198
1575
|
catch {
|
|
1199
1576
|
return false;
|
|
@@ -1205,8 +1582,8 @@ export class TurnRunner {
|
|
|
1205
1582
|
return true;
|
|
1206
1583
|
}
|
|
1207
1584
|
/** Push a frame onto the session's live chat stream from outside the turn (Python gateway notify callbacks). */
|
|
1208
|
-
emitToSession(sessionId, event, data) {
|
|
1209
|
-
this.sessionPuts.get(sessionId)?.(event, data);
|
|
1585
|
+
async emitToSession(sessionId, event, data) {
|
|
1586
|
+
await this.sessionPuts.get(sessionId)?.(event, data);
|
|
1210
1587
|
}
|
|
1211
1588
|
/** TAL-397: a turn that ends without its result keeps what the live stream already showed: counters and todo list. */
|
|
1212
1589
|
keepLiveState(s, streamId) {
|
|
@@ -1217,13 +1594,15 @@ export class TurnRunner {
|
|
|
1217
1594
|
if (todos)
|
|
1218
1595
|
s.extra[UNSETTLED_TODO_KEY] = todos;
|
|
1219
1596
|
}
|
|
1220
|
-
teardown(sessionId, streamId) {
|
|
1597
|
+
async teardown(sessionId, streamId) {
|
|
1598
|
+
this.runStatuses.delete(streamId);
|
|
1221
1599
|
this.liveUsage.delete(streamId);
|
|
1600
|
+
this.compressionSpans.delete(streamId);
|
|
1222
1601
|
this.liveTodos.delete(streamId);
|
|
1223
1602
|
const writer = this.writers.get(streamId);
|
|
1224
1603
|
if (writer) {
|
|
1225
1604
|
try {
|
|
1226
|
-
writer.close();
|
|
1605
|
+
(await writer.close());
|
|
1227
1606
|
}
|
|
1228
1607
|
catch { /* ignore */ }
|
|
1229
1608
|
this.writers.delete(streamId);
|
|
@@ -1252,21 +1631,27 @@ export class TurnRunner {
|
|
|
1252
1631
|
catch { /* best effort */ }
|
|
1253
1632
|
}
|
|
1254
1633
|
/** Journal files with a live writer (retention must leave them alone). */
|
|
1255
|
-
activeJournalPaths() {
|
|
1256
|
-
return new Set([...this.writers.values()].map((w) => this.deps.journal.pathFor(w.sessionId, w.runId)));
|
|
1257
|
-
}
|
|
1258
1634
|
// ── title ────────────────────────────────────────────────────────────────
|
|
1259
1635
|
/** Python `_generate_llm_session_title_via_aux`: aux-route statuses (`llm_aux`, `llm_aux_retry`, `llm_*_aux`). */
|
|
1260
1636
|
async llmTitle(s, userText, assistantText) {
|
|
1261
1637
|
const sidecar = this.deps.sidecar();
|
|
1262
1638
|
if (!sidecar)
|
|
1263
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);
|
|
1264
1643
|
const [qa, prompts] = titlePrompts(userText, assistantText);
|
|
1265
1644
|
let status = 'llm_error_aux';
|
|
1266
1645
|
for (const [idx, prompt] of prompts.entries()) {
|
|
1646
|
+
if ((await this.deps.service().isReadOnly(s)))
|
|
1647
|
+
return { title: '', status: 'read_only', rawPreview: '' };
|
|
1267
1648
|
let raw = '';
|
|
1268
1649
|
try {
|
|
1269
|
-
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
|
+
});
|
|
1270
1655
|
raw = str(result.text);
|
|
1271
1656
|
}
|
|
1272
1657
|
catch (error) {
|
|
@@ -1287,6 +1672,42 @@ export class TurnRunner {
|
|
|
1287
1672
|
}
|
|
1288
1673
|
return { title: '', status, rawPreview: '' };
|
|
1289
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
|
+
}
|
|
1290
1711
|
/**
|
|
1291
1712
|
* Python `generate_session_title_for_session`: on-demand title from the persisted transcript; never touches
|
|
1292
1713
|
* `llm_title_generated`. An explicit regenerate runs even with `auxiliary.title_generation.enabled` off (TAL-531).
|
|
@@ -1300,6 +1721,7 @@ export class TurnRunner {
|
|
|
1300
1721
|
let rawPreview = '';
|
|
1301
1722
|
if (assistantText)
|
|
1302
1723
|
({ title: next, status: llmStatus, rawPreview } = await this.llmTitle(s, userText, assistantText));
|
|
1724
|
+
this.deps.service().assertWritable(s);
|
|
1303
1725
|
if (next)
|
|
1304
1726
|
return { title: next, status: llmStatus, rawPreview };
|
|
1305
1727
|
const fallback = fallbackTitleFromExchange(userText, assistantText);
|
|
@@ -1307,6 +1729,15 @@ export class TurnRunner {
|
|
|
1307
1729
|
return { title: fallback, status: `local_summary:${llmStatus}`, rawPreview };
|
|
1308
1730
|
return { title: null, status: llmStatus || 'empty_title', rawPreview };
|
|
1309
1731
|
}
|
|
1732
|
+
/**
|
|
1733
|
+
* TAL-591 (Python `_queue_generated_title_for_imported_session`): a CLI session a mutation claims while it still
|
|
1734
|
+
* carries its default title gets the background title job at once instead of after its first WebUI turn.
|
|
1735
|
+
*/
|
|
1736
|
+
async titleClaimed(s) {
|
|
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 }))
|
|
1738
|
+
return;
|
|
1739
|
+
await this.backgroundTitle(s, () => Promise.resolve()).catch((error) => { this.deps.log(`[webui] imported session title failed for ${s.session_id}: ${error.message}`); });
|
|
1740
|
+
}
|
|
1310
1741
|
/** Python `_run_background_title_update` (spawned only when `_background_title_generation_inputs` is eligible). */
|
|
1311
1742
|
async backgroundTitle(s, put) {
|
|
1312
1743
|
const sessionId = s.session_id;
|
|
@@ -1317,27 +1748,34 @@ export class TurnRunner {
|
|
|
1317
1748
|
const eligible = ['Untitled', 'New Chat', ''].includes(placeholder) || placeholder === titleFrom(s.messages, '') || invalidExisting || defaultTitle(s);
|
|
1318
1749
|
const [userText, assistantText] = firstExchangeSnippets(s.messages);
|
|
1319
1750
|
// Python `_put_title_status`: empty reason/title/raw_preview keys are omitted.
|
|
1320
|
-
const status = (status, reason = '', title = '', rawPreview = '') => {
|
|
1321
|
-
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 } : {}) });
|
|
1322
1753
|
};
|
|
1323
|
-
|
|
1754
|
+
// Python ran the adaptive refresh only when the first-title job was not eligible.
|
|
1755
|
+
if (!eligible || (s.llm_title_generated && !invalidExisting) || !userText || !assistantText) {
|
|
1756
|
+
await this.refreshTitle(s, put, status);
|
|
1324
1757
|
return;
|
|
1758
|
+
}
|
|
1325
1759
|
if (s.manual_title) {
|
|
1326
|
-
status('skipped', 'manual_title', placeholder);
|
|
1760
|
+
(await status('skipped', 'manual_title', placeholder));
|
|
1327
1761
|
return;
|
|
1328
1762
|
}
|
|
1329
1763
|
// A missing config.yaml reads as `{}` (enabled); an unreadable one cannot confirm the user has not opted out.
|
|
1330
1764
|
const cfg = await this.deps.profileConfig?.(s.profile ?? null);
|
|
1331
1765
|
if (!cfg) {
|
|
1332
|
-
status('skipped', 'config_unavailable', placeholder);
|
|
1766
|
+
(await status('skipped', 'config_unavailable', placeholder));
|
|
1333
1767
|
return;
|
|
1334
1768
|
}
|
|
1335
1769
|
if (!titleGenerationEnabled(cfg)) {
|
|
1336
|
-
status('skipped', 'title_generation_disabled', placeholder);
|
|
1770
|
+
(await status('skipped', 'title_generation_disabled', placeholder));
|
|
1337
1771
|
return;
|
|
1338
1772
|
}
|
|
1339
1773
|
const generated = await this.llmTitle(s, userText, assistantText);
|
|
1340
1774
|
const { status: llmStatus, rawPreview } = generated;
|
|
1775
|
+
if (llmStatus === 'read_only') {
|
|
1776
|
+
(await status('skipped', 'read_only', placeholder));
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1341
1779
|
let next = generated.title;
|
|
1342
1780
|
let source = llmStatus;
|
|
1343
1781
|
if (!next) {
|
|
@@ -1350,23 +1788,30 @@ export class TurnRunner {
|
|
|
1350
1788
|
const fallbackReason = source === 'fallback' && llmStatus ? `local_summary:${llmStatus}` : 'local_summary';
|
|
1351
1789
|
let current = s;
|
|
1352
1790
|
try {
|
|
1353
|
-
current = this.deps.store.get(sessionId);
|
|
1791
|
+
current = (await this.deps.store.get(sessionId));
|
|
1354
1792
|
}
|
|
1355
1793
|
catch {
|
|
1794
|
+
// A session deleted while the title prompt ran stays deleted.
|
|
1795
|
+
if ((await this.deps.store.wasDeleted(sessionId)))
|
|
1796
|
+
return;
|
|
1356
1797
|
current = s;
|
|
1357
1798
|
}
|
|
1799
|
+
if ((await this.deps.service().isReadOnly(current))) {
|
|
1800
|
+
(await status('skipped', 'read_only', str(current.title).trim()));
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1358
1803
|
let effective = str(current.title).trim();
|
|
1359
1804
|
let wrote = false;
|
|
1360
1805
|
if (next) {
|
|
1361
1806
|
const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title) || defaultTitle(current);
|
|
1362
1807
|
if (current.manual_title || !stillAuto) {
|
|
1363
|
-
status('skipped', 'manual_title', effective);
|
|
1808
|
+
(await status('skipped', 'manual_title', effective));
|
|
1364
1809
|
return;
|
|
1365
1810
|
}
|
|
1366
1811
|
if (next !== effective) {
|
|
1367
1812
|
current.title = next;
|
|
1368
1813
|
markSessionTitleGenerated(current);
|
|
1369
|
-
this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1814
|
+
await this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1370
1815
|
// Python `sync_session_title` after generation: the state.db row follows when `sync_to_insights` is on.
|
|
1371
1816
|
await this.deps.syncTitle?.(current);
|
|
1372
1817
|
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
@@ -1375,11 +1820,80 @@ export class TurnRunner {
|
|
|
1375
1820
|
}
|
|
1376
1821
|
}
|
|
1377
1822
|
if (wrote) {
|
|
1378
|
-
status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
|
|
1379
|
-
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 });
|
|
1380
1825
|
}
|
|
1381
1826
|
else
|
|
1382
|
-
status('skipped', source || 'unchanged', effective, rawPreview);
|
|
1827
|
+
(await status('skipped', source || 'unchanged', effective, rawPreview));
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Python `_maybe_schedule_title_refresh` + `_run_background_title_refresh`: every N exchanges a generated title is
|
|
1831
|
+
* regenerated from the latest exchange; a manual title is never touched.
|
|
1832
|
+
*/
|
|
1833
|
+
async refreshTitle(s, put, status) {
|
|
1834
|
+
const every = this.deps.titleRefreshEvery?.() ?? 0;
|
|
1835
|
+
const exchanges = countExchanges(s.messages);
|
|
1836
|
+
if (every <= 0 || exchanges <= 0 || exchanges % every !== 0)
|
|
1837
|
+
return;
|
|
1838
|
+
const [userText, assistantText] = latestExchangeSnippets(s.messages);
|
|
1839
|
+
if (!userText || !assistantText)
|
|
1840
|
+
return;
|
|
1841
|
+
// Admission is released before title work, so an earlier turn's refresh may still be running: refreshes apply in
|
|
1842
|
+
// turn order and each reads the title its predecessor wrote.
|
|
1843
|
+
const sessionId = s.session_id;
|
|
1844
|
+
const run = (this.titleRefreshes.get(sessionId) ?? Promise.resolve()).catch(() => undefined).then(async () => { await this.applyTitleRefresh(sessionId, s, userText, assistantText, put, status); });
|
|
1845
|
+
this.titleRefreshes.set(sessionId, run);
|
|
1846
|
+
try {
|
|
1847
|
+
await run;
|
|
1848
|
+
}
|
|
1849
|
+
finally {
|
|
1850
|
+
if (this.titleRefreshes.get(sessionId) === run)
|
|
1851
|
+
this.titleRefreshes.delete(sessionId);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
async applyTitleRefresh(sessionId, s, userText, assistantText, put, status) {
|
|
1855
|
+
const read = async () => { try {
|
|
1856
|
+
return (await this.deps.store.get(sessionId));
|
|
1857
|
+
}
|
|
1858
|
+
catch {
|
|
1859
|
+
return s;
|
|
1860
|
+
} };
|
|
1861
|
+
const latest = (await read());
|
|
1862
|
+
const placeholder = str(latest.title).trim();
|
|
1863
|
+
if (['Untitled', 'New Chat', ''].includes(placeholder) || latest.manual_title || !latest.llm_title_generated)
|
|
1864
|
+
return;
|
|
1865
|
+
const cfg = await this.deps.profileConfig?.(latest.profile ?? null);
|
|
1866
|
+
if (!cfg || !titleGenerationEnabled(cfg)) {
|
|
1867
|
+
(await status('refresh_skipped', cfg ? 'title_generation_disabled' : 'config_unavailable', placeholder));
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
const { title: next, status: llmStatus, rawPreview } = await this.llmTitle(latest, userText, assistantText);
|
|
1871
|
+
if (!next) {
|
|
1872
|
+
(await status('refresh_skipped', llmStatus || 'empty', placeholder, rawPreview));
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const normalize = (value) => value.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1876
|
+
if (normalize(next) === normalize(placeholder)) {
|
|
1877
|
+
(await status('refresh_skipped', 'same_title', placeholder, rawPreview));
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const current = (await read());
|
|
1881
|
+
// The user may have renamed the session while the title prompt ran.
|
|
1882
|
+
if (current.manual_title || str(current.title).trim() !== placeholder) {
|
|
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()));
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
current.title = next;
|
|
1891
|
+
markSessionTitleGenerated(current);
|
|
1892
|
+
await this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1893
|
+
await this.deps.syncTitle?.(current);
|
|
1894
|
+
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
1895
|
+
await status('refreshed', llmStatus, next, rawPreview);
|
|
1896
|
+
await put('title', { session_id: sessionId, title: next });
|
|
1383
1897
|
}
|
|
1384
1898
|
// ── cancel / steer ───────────────────────────────────────────────────────
|
|
1385
1899
|
/**
|
|
@@ -1387,6 +1901,26 @@ export class TurnRunner {
|
|
|
1387
1901
|
* `steer_withdrawn` (stopped) this Stop emitted, for the cancel response (TAL-426).
|
|
1388
1902
|
*/
|
|
1389
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) {
|
|
1390
1924
|
const channel = this.registry.peek(streamId);
|
|
1391
1925
|
const run = this.registry.activeRuns.get(streamId);
|
|
1392
1926
|
if (!channel && !run)
|
|
@@ -1421,24 +1955,24 @@ export class TurnRunner {
|
|
|
1421
1955
|
if (sessionId) {
|
|
1422
1956
|
let current = null;
|
|
1423
1957
|
try {
|
|
1424
|
-
current = this.deps.store.get(sessionId);
|
|
1958
|
+
current = (await this.deps.store.get(sessionId));
|
|
1425
1959
|
}
|
|
1426
1960
|
catch {
|
|
1427
1961
|
current = null;
|
|
1428
1962
|
}
|
|
1429
1963
|
if (current?.active_stream_id === streamId) {
|
|
1430
|
-
this.finalizeCancelled(current, streamId, run?.ephemeral, checkpoint);
|
|
1964
|
+
await this.finalizeCancelled(current, streamId, run?.ephemeral, checkpoint);
|
|
1431
1965
|
}
|
|
1432
1966
|
}
|
|
1433
1967
|
// The worker may already have written its terminal row while the interrupt was in flight; the stream is then
|
|
1434
1968
|
// gone from the registry and this path must not add a second one.
|
|
1435
1969
|
if (channel && this.registry.streams.has(streamId)) {
|
|
1436
1970
|
const writer = this.writers.get(streamId) ?? null;
|
|
1437
|
-
const emit = (event, data) => {
|
|
1971
|
+
const emit = async (event, data) => {
|
|
1438
1972
|
let eventId = null;
|
|
1439
1973
|
if (writer) {
|
|
1440
1974
|
try {
|
|
1441
|
-
eventId = writer.appendSseEvent(event, data).event_id;
|
|
1975
|
+
eventId = (await writer.appendSseEvent(event, data)).event_id;
|
|
1442
1976
|
}
|
|
1443
1977
|
catch {
|
|
1444
1978
|
eventId = null;
|
|
@@ -1447,8 +1981,10 @@ export class TurnRunner {
|
|
|
1447
1981
|
channel.put([event, data, eventId]);
|
|
1448
1982
|
};
|
|
1449
1983
|
for (const [event, data] of steerEvents)
|
|
1450
|
-
emit(event, data);
|
|
1451
|
-
|
|
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)));
|
|
1452
1988
|
this.registry.streams.delete(streamId);
|
|
1453
1989
|
this.registry.liveIds.delete(streamId);
|
|
1454
1990
|
}
|
|
@@ -1464,7 +2000,7 @@ export class TurnRunner {
|
|
|
1464
2000
|
const streamId = this.registry.activeRunStreamForSession(sessionId);
|
|
1465
2001
|
if (!streamId || this.registry.activeRuns.get(streamId)?.origin !== 'background')
|
|
1466
2002
|
return false;
|
|
1467
|
-
await this.
|
|
2003
|
+
await this.stop(streamId);
|
|
1468
2004
|
let timer;
|
|
1469
2005
|
await Promise.race([this.workers.get(streamId), new Promise((resolve) => { timer = setTimeout(resolve, BACKGROUND_UNWIND_WAIT_MS); })]);
|
|
1470
2006
|
clearTimeout(timer);
|
|
@@ -1473,7 +2009,7 @@ export class TurnRunner {
|
|
|
1473
2009
|
async steer(sessionId, text, displayText, steerId) {
|
|
1474
2010
|
let s = null;
|
|
1475
2011
|
try {
|
|
1476
|
-
s = this.deps.store.get(sessionId, { metadataOnly: true });
|
|
2012
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
1477
2013
|
}
|
|
1478
2014
|
catch {
|
|
1479
2015
|
s = null;
|
|
@@ -1492,7 +2028,7 @@ export class TurnRunner {
|
|
|
1492
2028
|
return { accepted: false, fallback: 'no_cached_agent', stream_id: null };
|
|
1493
2029
|
// Register provisionally before the RPC: the Agent may apply the steer and finish the turn before the steer
|
|
1494
2030
|
// reply arrives, and the turn's finalisation must then already see the record. A rejection removes it again.
|
|
1495
|
-
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));
|
|
1496
2032
|
}
|
|
1497
2033
|
async steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId) {
|
|
1498
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 };
|
|
@@ -1524,7 +2060,7 @@ export class TurnRunner {
|
|
|
1524
2060
|
record.can_redirect = result.can_redirect === true;
|
|
1525
2061
|
// TAL-424: every client shows it until the Agent takes it (unless it already did while the reply was in flight).
|
|
1526
2062
|
if (this.steers.get(activeStreamId)?.includes(record))
|
|
1527
|
-
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
2063
|
+
(await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record)));
|
|
1528
2064
|
}
|
|
1529
2065
|
return { accepted: result.accepted, fallback: result.accepted ? null : (result.fallback ?? 'not_running'), stream_id: activeStreamId, steer_id: steerId };
|
|
1530
2066
|
}
|
|
@@ -1593,10 +2129,10 @@ export class TurnRunner {
|
|
|
1593
2129
|
return boundary;
|
|
1594
2130
|
}
|
|
1595
2131
|
/** A steer is saved as it enters the stream, so a reload mid-turn already has it; the terminal save re-places it. */
|
|
1596
|
-
saveConsumedSteers(sessionId, streamId) {
|
|
2132
|
+
async saveConsumedSteers(sessionId, streamId) {
|
|
1597
2133
|
let s;
|
|
1598
2134
|
try {
|
|
1599
|
-
s = this.deps.store.get(sessionId);
|
|
2135
|
+
s = (await this.deps.store.get(sessionId));
|
|
1600
2136
|
}
|
|
1601
2137
|
catch {
|
|
1602
2138
|
return;
|
|
@@ -1604,7 +2140,7 @@ export class TurnRunner {
|
|
|
1604
2140
|
if (s.active_stream_id !== streamId || this.placeConsumedSteers(s, streamId, s.pending_started_at) === null)
|
|
1605
2141
|
return;
|
|
1606
2142
|
try {
|
|
1607
|
-
this.deps.store.save(s, { touchUpdatedAt: false });
|
|
2143
|
+
(await this.deps.store.save(s, { touchUpdatedAt: false }));
|
|
1608
2144
|
}
|
|
1609
2145
|
catch (error) {
|
|
1610
2146
|
this.deps.log(`[webui] WARNING: failed to save consumed steer for ${sessionId}: ${error.message}`);
|
|
@@ -1658,30 +2194,24 @@ export class TurnRunner {
|
|
|
1658
2194
|
return { events, leftovers: [...(unmatched ? [unmatched] : []), ...left.map((r) => r.text)] };
|
|
1659
2195
|
if (unmatched)
|
|
1660
2196
|
events.push(['steer_withdrawn', { steer_id: null, reason: 'stopped', text: unmatched }]);
|
|
1661
|
-
// ponytail: legacy `pending_steer_leftover` keeps today's Web and App from losing a stopped steer (they requeue it);
|
|
1662
|
-
// drop it once TAL-425 / TAL-426 put `steer_withdrawn` text back in the composer.
|
|
1663
|
-
for (const r of left)
|
|
1664
|
-
events.push(['pending_steer_leftover', { steer_id: r.steer_id, stream_id: streamId, text: r.text, display_text: r.display_text, leftover_at: this.deps.now() }]);
|
|
1665
|
-
if (unmatched)
|
|
1666
|
-
events.push(['pending_steer_leftover', { stream_id: streamId, text: unmatched }]);
|
|
1667
2197
|
return { events, leftovers: [] };
|
|
1668
2198
|
}
|
|
1669
2199
|
/**
|
|
1670
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.
|
|
1671
2201
|
* Should another turn already hold the session, they steer into that one instead, so no text is lost.
|
|
1672
2202
|
*/
|
|
1673
|
-
startSteerFollowUp(sessionId, texts) {
|
|
2203
|
+
async startSteerFollowUp(sessionId, texts) {
|
|
1674
2204
|
if (!texts.length)
|
|
1675
2205
|
return;
|
|
1676
2206
|
let s;
|
|
1677
2207
|
try {
|
|
1678
|
-
s = this.deps.store.get(sessionId);
|
|
2208
|
+
s = (await this.deps.store.get(sessionId));
|
|
1679
2209
|
}
|
|
1680
2210
|
catch {
|
|
1681
2211
|
return;
|
|
1682
2212
|
}
|
|
1683
2213
|
const msg = texts.join('\n');
|
|
1684
|
-
const started = this.
|
|
2214
|
+
const started = (await this.startServerTurn(s, { msg }));
|
|
1685
2215
|
if (!started._status || started._status < 400)
|
|
1686
2216
|
return;
|
|
1687
2217
|
if (started._status === 409 && started.active_stream_id) {
|
|
@@ -1709,61 +2239,83 @@ export class TurnRunner {
|
|
|
1709
2239
|
// Only a known goal turn shows progress up front; any other turn shows nothing unless a goal turns out to be active.
|
|
1710
2240
|
const goalTurn = this.registry.goalRelated.has(streamId);
|
|
1711
2241
|
if (goalTurn)
|
|
1712
|
-
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' }));
|
|
1713
2243
|
let decision;
|
|
1714
2244
|
try {
|
|
1715
|
-
|
|
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 });
|
|
1716
2248
|
}
|
|
1717
2249
|
catch (error) {
|
|
1718
2250
|
this.deps.log(`[webui] WARNING: goal evaluation failed for ${sessionId}: ${error.message}`);
|
|
1719
2251
|
if (goalTurn)
|
|
1720
|
-
put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
2252
|
+
(await put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' }));
|
|
1721
2253
|
return;
|
|
1722
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);
|
|
1723
2258
|
if (!goalTurn && decision.verdict === 'inactive')
|
|
1724
2259
|
return;
|
|
1725
2260
|
const prompt = str(decision.continuation_prompt).trim();
|
|
1726
2261
|
const proceed = decision.should_continue && Boolean(prompt);
|
|
1727
2262
|
const status = { message: decision.message, message_key: decision.message_key, message_args: decision.message_args ?? [], decision: decision.verdict };
|
|
1728
|
-
|
|
1729
|
-
|
|
2263
|
+
if (!proceed) {
|
|
2264
|
+
(await put('goal', { session_id: sessionId, state: 'idle', ...status }));
|
|
1730
2265
|
return;
|
|
2266
|
+
}
|
|
1731
2267
|
let current;
|
|
1732
2268
|
try {
|
|
1733
|
-
current = this.deps.store.get(sessionId);
|
|
2269
|
+
current = (await this.deps.store.get(sessionId));
|
|
1734
2270
|
}
|
|
1735
2271
|
catch {
|
|
1736
2272
|
return;
|
|
1737
2273
|
}
|
|
1738
|
-
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 });
|
|
1739
2281
|
if (started._status === 409 && started.active_stream_id) {
|
|
1740
2282
|
this.registry.goalRelated.add(started.active_stream_id);
|
|
1741
2283
|
return;
|
|
1742
2284
|
}
|
|
1743
2285
|
if (started._status !== undefined && started._status >= 400) {
|
|
1744
2286
|
this.deps.log(`[webui] WARNING: goal continuation for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
|
|
1745
|
-
put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
2287
|
+
await put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
1746
2288
|
return;
|
|
1747
2289
|
}
|
|
1748
|
-
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 });
|
|
1749
2291
|
}
|
|
1750
2292
|
/** The Agent's pending steer text settles which steers it has taken: those become `steer_consumed` and their rows. */
|
|
1751
|
-
settleConsumedSteers(sessionId, streamId, pendingText) {
|
|
2293
|
+
async settleConsumedSteers(sessionId, streamId, pendingText) {
|
|
1752
2294
|
for (const record of this.takeConsumedSteers(streamId, pendingText, { keepLeftovers: true }))
|
|
1753
|
-
this.emitToSession(sessionId, 'steer_consumed', record);
|
|
1754
|
-
this.saveConsumedSteers(sessionId, streamId);
|
|
2295
|
+
(await this.emitToSession(sessionId, 'steer_consumed', record));
|
|
2296
|
+
await this.saveConsumedSteers(sessionId, streamId);
|
|
1755
2297
|
}
|
|
1756
2298
|
/** What every client shows for a pending steer; the server decides which actions it offers. */
|
|
1757
2299
|
pendingSteerView(r) {
|
|
1758
2300
|
const open = r.state === 'pending';
|
|
1759
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 } };
|
|
1760
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
|
+
}
|
|
1761
2310
|
/** TAL-424: the active stream's pending steers, oldest first (session detail `pending_steers`). */
|
|
1762
2311
|
pendingSteers(streamId) {
|
|
1763
2312
|
return (this.steers.get(streamId) ?? []).map((r) => this.pendingSteerView(r));
|
|
1764
2313
|
}
|
|
1765
|
-
withSteerChain(streamId, fn) {
|
|
1766
|
-
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
|
+
});
|
|
1767
2319
|
const settled = run.catch(() => undefined).then(() => { if (this.steerChains.get(streamId) === settled)
|
|
1768
2320
|
this.steerChains.delete(streamId); });
|
|
1769
2321
|
this.steerChains.set(streamId, settled);
|
|
@@ -1777,7 +2329,7 @@ export class TurnRunner {
|
|
|
1777
2329
|
async rewriteSteer(sessionId, steerId, fallback, rewrite) {
|
|
1778
2330
|
let s = null;
|
|
1779
2331
|
try {
|
|
1780
|
-
s = this.deps.store.get(sessionId, { metadataOnly: true });
|
|
2332
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
1781
2333
|
}
|
|
1782
2334
|
catch {
|
|
1783
2335
|
s = null;
|
|
@@ -1785,21 +2337,21 @@ export class TurnRunner {
|
|
|
1785
2337
|
const streamId = s?.active_stream_id;
|
|
1786
2338
|
const sidecar = this.deps.sidecar();
|
|
1787
2339
|
if (!streamId || !sidecar)
|
|
1788
|
-
return fallback;
|
|
1789
|
-
return this.withSteerChain(streamId, async () => {
|
|
2340
|
+
return fallback('not_running');
|
|
2341
|
+
return this.withSteerChain(streamId, sessionId, async () => {
|
|
1790
2342
|
const records = this.steers.get(streamId) ?? [];
|
|
1791
2343
|
const index = records.findIndex((r) => r.steer_id === steerId);
|
|
1792
2344
|
if (index < 0 || records[index].state !== 'pending')
|
|
1793
|
-
return fallback;
|
|
2345
|
+
return fallback('taken');
|
|
1794
2346
|
// In flight before the sidecar call, so a pending-text report racing its reply is deferred too.
|
|
1795
2347
|
let finished = () => undefined;
|
|
1796
2348
|
this.steerRewrites.set(streamId, new Promise((resolve) => { finished = resolve; }));
|
|
1797
|
-
let outcome = { result: fallback, changed: false };
|
|
2349
|
+
let outcome = { result: fallback('failed'), changed: false };
|
|
1798
2350
|
try {
|
|
1799
2351
|
outcome = await rewrite(sidecar, streamId, records, index);
|
|
1800
2352
|
}
|
|
1801
2353
|
catch {
|
|
1802
|
-
outcome = { result: fallback, changed: false };
|
|
2354
|
+
outcome = { result: fallback('failed'), changed: false };
|
|
1803
2355
|
}
|
|
1804
2356
|
finally {
|
|
1805
2357
|
this.steerRewrites.delete(streamId);
|
|
@@ -1807,7 +2359,7 @@ export class TurnRunner {
|
|
|
1807
2359
|
const deferred = this.deferredPendingSteer.get(streamId);
|
|
1808
2360
|
this.deferredPendingSteer.delete(streamId);
|
|
1809
2361
|
if (deferred !== undefined && !outcome.changed)
|
|
1810
|
-
this.settleConsumedSteers(sessionId, streamId, deferred);
|
|
2362
|
+
(await this.settleConsumedSteers(sessionId, streamId, deferred));
|
|
1811
2363
|
}
|
|
1812
2364
|
return outcome.result;
|
|
1813
2365
|
});
|
|
@@ -1822,54 +2374,61 @@ export class TurnRunner {
|
|
|
1822
2374
|
* TAL-424 Edit and Cancel: take a pending steer back before the Agent takes it. `withdrawn: false` (and nothing
|
|
1823
2375
|
* emitted) when it is unknown, already taken or being sent; a taken one then settles as consumed.
|
|
1824
2376
|
*/
|
|
1825
|
-
withdrawSteer(sessionId, steerId, reason) {
|
|
1826
|
-
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) => {
|
|
1827
2379
|
const record = records[index];
|
|
1828
2380
|
const reply = await sidecar.call('chat.steer_withdraw', { stream_id: streamId, pending: records.map((r) => r.text), index });
|
|
1829
2381
|
if (!reply.withdrawn)
|
|
1830
2382
|
return { result: { withdrawn: false }, changed: false };
|
|
1831
2383
|
this.dropSteer(streamId, record);
|
|
1832
|
-
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 });
|
|
1833
2385
|
return { result: { withdrawn: true, text: record.display_text }, changed: true };
|
|
1834
|
-
});
|
|
2386
|
+
}));
|
|
1835
2387
|
}
|
|
1836
2388
|
/**
|
|
1837
2389
|
* TAL-424 Send now: deliver a pending steer with the Agent's redirect. A model request restarts with it (consumed at
|
|
1838
2390
|
* once); during tools it goes last on the Agent's queue and stays `sending_now` until taken; with nothing live it
|
|
1839
2391
|
* stays pending.
|
|
1840
2392
|
*/
|
|
1841
|
-
sendSteerNow(sessionId, steerId) {
|
|
1842
|
-
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) => {
|
|
1843
2395
|
const record = records[index];
|
|
1844
2396
|
if (!record.can_redirect)
|
|
1845
|
-
return { result: { redirected: false }, changed: false };
|
|
2397
|
+
return { result: { redirected: false, outcome: 'failed' }, changed: false };
|
|
1846
2398
|
const reply = await sidecar.call('chat.steer_now', { stream_id: streamId, pending: records.map((r) => r.text), index });
|
|
1847
2399
|
if (!reply.withdrawn)
|
|
1848
|
-
return { result: { redirected: false }, changed: false };
|
|
2400
|
+
return { result: { redirected: false, outcome: reply.reason === 'not_running' ? 'not_running' : 'taken' }, changed: false };
|
|
1849
2401
|
if (!reply.redirected) {
|
|
1850
2402
|
// The Agent's queue changed meanwhile, so it went back last: clients follow the new order.
|
|
1851
2403
|
if (reply.requeued === 'last') {
|
|
1852
2404
|
this.dropSteer(streamId, record);
|
|
1853
2405
|
records.push(record);
|
|
1854
|
-
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
2406
|
+
(await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record)));
|
|
1855
2407
|
}
|
|
1856
|
-
return { result: { redirected: false }, changed: true };
|
|
2408
|
+
return { result: { redirected: false, outcome: 'next_step' }, changed: true };
|
|
1857
2409
|
}
|
|
1858
2410
|
this.dropSteer(streamId, record);
|
|
1859
2411
|
if (reply.delivery === 'steer') {
|
|
1860
2412
|
record.state = 'sending_now';
|
|
1861
2413
|
records.push(record);
|
|
1862
|
-
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
2414
|
+
await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
1863
2415
|
}
|
|
1864
2416
|
else {
|
|
1865
|
-
this.emitToSession(sessionId, 'steer_consumed', this.consumedSteerPayload(record, { redirected: true }));
|
|
1866
|
-
this.saveConsumedSteers(sessionId, streamId);
|
|
2417
|
+
await this.emitToSession(sessionId, 'steer_consumed', this.consumedSteerPayload(record, { redirected: true }));
|
|
2418
|
+
await this.saveConsumedSteers(sessionId, streamId);
|
|
1867
2419
|
}
|
|
1868
|
-
return { result: { redirected: true }, changed: true };
|
|
1869
|
-
});
|
|
2420
|
+
return { result: { redirected: true, outcome: 'delivered' }, changed: true };
|
|
2421
|
+
}));
|
|
1870
2422
|
}
|
|
1871
2423
|
// ── approvals / clarify ──────────────────────────────────────────────────
|
|
1872
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));
|
|
1873
2432
|
// Python `_enable_session_yolo_and_release_pending`: a stale card id while another approval is parked
|
|
1874
2433
|
// must not commit YOLO for the whole session — fail closed with the relay envelope instead.
|
|
1875
2434
|
if (enableYolo && approvalId && !this.deps.pending.hasApprovalId(sessionId, approvalId) && this.deps.pending.hasPendingApproval(sessionId)) {
|
|
@@ -1882,7 +2441,7 @@ export class TurnRunner {
|
|
|
1882
2441
|
const relayFailure = (message) => ({ ok: false, choice, error: `The Agent sidecar did not accept the approval (${message}); retry in a moment.`, _status: 503 });
|
|
1883
2442
|
let s = null;
|
|
1884
2443
|
try {
|
|
1885
|
-
s = this.deps.store.get(sessionId, { metadataOnly: true });
|
|
2444
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
1886
2445
|
}
|
|
1887
2446
|
catch {
|
|
1888
2447
|
s = null;
|
|
@@ -1890,7 +2449,9 @@ export class TurnRunner {
|
|
|
1890
2449
|
const profileHome = this.deps.profileHome(s?.profile ?? null);
|
|
1891
2450
|
let resolved = false;
|
|
1892
2451
|
let withdrawn = false;
|
|
1893
|
-
if (entry)
|
|
2452
|
+
if (entry?._injected === true)
|
|
2453
|
+
resolved = Boolean(this.deps.pending.resolveApproval(sessionId, str(entry.approval_id)).entry);
|
|
2454
|
+
else if (entry) {
|
|
1894
2455
|
if (!sidecar)
|
|
1895
2456
|
return relayFailure('not running');
|
|
1896
2457
|
try {
|
|
@@ -1922,7 +2483,7 @@ export class TurnRunner {
|
|
|
1922
2483
|
return relayFailure(str(error.message));
|
|
1923
2484
|
}
|
|
1924
2485
|
}
|
|
1925
|
-
this.deps.service().setYolo(sessionId, true);
|
|
2486
|
+
await this.deps.service().setYolo(sessionId, true);
|
|
1926
2487
|
return null;
|
|
1927
2488
|
});
|
|
1928
2489
|
if (failure)
|
|
@@ -1956,17 +2517,21 @@ export class TurnRunner {
|
|
|
1956
2517
|
return;
|
|
1957
2518
|
}
|
|
1958
2519
|
if (this.deps.pending.dropApprovals(sessionId, new Set(before.filter((id) => !live.has(id)))).length)
|
|
1959
|
-
this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId));
|
|
2520
|
+
(await this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId)));
|
|
1960
2521
|
}
|
|
1961
2522
|
withYoloLock(sessionId, fn) {
|
|
1962
|
-
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
|
+
});
|
|
1963
2527
|
const settled = run.catch(() => undefined).then(() => { if (this.yoloChains.get(sessionId) === settled)
|
|
1964
2528
|
this.yoloChains.delete(sessionId); });
|
|
1965
2529
|
this.yoloChains.set(sessionId, settled);
|
|
1966
2530
|
return run;
|
|
1967
2531
|
}
|
|
1968
2532
|
/** Python `set_session_yolo_enabled(False)`: the sidecar must drop its per-session YOLO state before the local flag clears. */
|
|
1969
|
-
disableYolo(sessionId) {
|
|
2533
|
+
async disableYolo(sessionId) {
|
|
2534
|
+
sessionId = (await this.deps.service().idForMutation(sessionId));
|
|
1970
2535
|
return this.withYoloLock(sessionId, async () => {
|
|
1971
2536
|
const sidecar = this.deps.sidecar();
|
|
1972
2537
|
if (sidecar) {
|
|
@@ -1977,7 +2542,7 @@ export class TurnRunner {
|
|
|
1977
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 };
|
|
1978
2543
|
}
|
|
1979
2544
|
}
|
|
1980
|
-
return this.deps.service().setYolo(sessionId, false);
|
|
2545
|
+
return (await this.deps.service().setYolo(sessionId, false));
|
|
1981
2546
|
});
|
|
1982
2547
|
}
|
|
1983
2548
|
/**
|
|
@@ -1986,12 +2551,26 @@ export class TurnRunner {
|
|
|
1986
2551
|
* did not acknowledge (prompt retained).
|
|
1987
2552
|
*/
|
|
1988
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));
|
|
1989
2559
|
const entry = this.deps.pending.peekClarify(sessionId, clarifyId);
|
|
1990
2560
|
if (!entry)
|
|
1991
2561
|
return { ok: false, stale: true };
|
|
1992
|
-
|
|
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);
|
|
1993
2567
|
if (response === null)
|
|
1994
2568
|
return { ok: false, invalid: true };
|
|
2569
|
+
// A test-hook prompt has no parked Agent waiter: it is answered here.
|
|
2570
|
+
if (entry._injected === true) {
|
|
2571
|
+
this.deps.pending.resolveClarify(sessionId, str(entry.clarify_id));
|
|
2572
|
+
return vault ? { ok: true } : { ok: true, response };
|
|
2573
|
+
}
|
|
1995
2574
|
const sidecar = this.deps.sidecar();
|
|
1996
2575
|
if (!sidecar)
|
|
1997
2576
|
return { ok: false, error: 'The Agent sidecar is not running; retry in a moment.' };
|
|
@@ -2000,14 +2579,16 @@ export class TurnRunner {
|
|
|
2000
2579
|
ok = (await sidecar.call('clarify.respond', { session_id: sessionId, clarify_id: str(entry.clarify_id), response })).ok;
|
|
2001
2580
|
}
|
|
2002
2581
|
catch (error) {
|
|
2582
|
+
if (vault)
|
|
2583
|
+
return { ok: false, error: 'The Agent sidecar did not accept the secure answer; retry in a moment.' };
|
|
2003
2584
|
return { ok: false, error: `The Agent sidecar did not accept the answer (${str(error.message)}); retry in a moment.` };
|
|
2004
2585
|
}
|
|
2005
2586
|
if (!ok)
|
|
2006
2587
|
return { ok: false, stale: true };
|
|
2007
2588
|
const { head } = this.deps.pending.resolveClarify(sessionId, str(entry.clarify_id));
|
|
2008
2589
|
if (head)
|
|
2009
|
-
this.emitToSession(sessionId, 'clarify', head);
|
|
2010
|
-
return { ok: true, response };
|
|
2590
|
+
(await this.emitToSession(sessionId, 'clarify', head));
|
|
2591
|
+
return vault ? { ok: true } : { ok: true, response };
|
|
2011
2592
|
}
|
|
2012
2593
|
}
|
|
2013
2594
|
/** The turn's answer: the last non-error assistant reply with content. */
|