@maudecode/talaria-web 1.18.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_release.json +8 -8
- package/dist/api/auth-raw.js +16 -16
- package/dist/api/auth-raw.js.map +1 -1
- package/dist/api/automation-raw.d.ts +2 -2
- package/dist/api/automation-raw.js +93 -38
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +99 -17
- package/dist/api/automation-router.js +22 -14
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +1433 -37
- package/dist/api/chat-router.js +205 -90
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +6 -2
- package/dist/api/git-router.js +30 -30
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +3 -3
- package/dist/api/raw-routes.js +76 -62
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +5 -5
- package/dist/api/router.js +22 -22
- package/dist/api/router.js.map +1 -1
- package/dist/api/session-visibility.d.ts +4 -4
- package/dist/api/session-visibility.js +26 -16
- package/dist/api/session-visibility.js.map +1 -1
- package/dist/api/sessions-router.d.ts +2635 -151
- package/dist/api/sessions-router.js +193 -177
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +450 -11
- package/dist/api/settings-router.js +102 -49
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +2 -2
- package/dist/api/sse-routes.js +102 -80
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.d.ts +5 -0
- package/dist/api/tools-raw.js +58 -33
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +963 -109
- package/dist/api/tools-router.js +426 -72
- 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 +80 -57
- package/dist/app.js.map +1 -1
- package/dist/auth/crypto.d.ts +3 -0
- package/dist/auth/crypto.js +18 -0
- package/dist/auth/crypto.js.map +1 -1
- package/dist/auth/gate.js +14 -9
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/passkeys.d.ts +15 -14
- package/dist/auth/passkeys.js +89 -91
- package/dist/auth/passkeys.js.map +1 -1
- package/dist/auth/store.d.ts +17 -7
- package/dist/auth/store.js +93 -52
- package/dist/auth/store.js.map +1 -1
- package/dist/bin/talaria-web.js +12 -81
- package/dist/bin/talaria-web.js.map +1 -1
- package/dist/cli/ctl.js +8 -2
- package/dist/cli/ctl.js.map +1 -1
- package/dist/cli/dotenv.d.ts +5 -0
- package/dist/cli/dotenv.js +14 -2
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/cli/launcher.js +5 -5
- package/dist/cli/launcher.js.map +1 -1
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +90 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/supervise.d.ts +24 -0
- package/dist/cli/supervise.js +147 -6
- package/dist/cli/supervise.js.map +1 -1
- package/dist/config/agent-config.d.ts +37 -11
- package/dist/config/agent-config.js +146 -64
- package/dist/config/agent-config.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +14 -5
- package/dist/config.js.map +1 -1
- package/dist/fs/atomic.d.ts +4 -0
- package/dist/fs/atomic.js +19 -2
- package/dist/fs/atomic.js.map +1 -1
- package/dist/fs/stat.d.ts +7 -0
- package/dist/fs/stat.js +27 -0
- package/dist/fs/stat.js.map +1 -0
- package/dist/http/context.d.ts +28 -9
- package/dist/http/context.js +77 -25
- package/dist/http/context.js.map +1 -1
- package/dist/onboarding.d.ts +2 -2
- package/dist/onboarding.js +10 -8
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.d.ts +12 -4
- package/dist/profiles/profiles.js +78 -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 +85 -14
- package/dist/providers/catalog.js +502 -146
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/env-file.d.ts +10 -3
- package/dist/providers/env-file.js +24 -75
- package/dist/providers/env-file.js.map +1 -1
- package/dist/providers/runtime-env.d.ts +4 -2
- package/dist/providers/runtime-env.js +9 -6
- package/dist/providers/runtime-env.js.map +1 -1
- package/dist/providers/tables.d.ts +3 -1
- package/dist/providers/tables.js +4 -2
- package/dist/providers/tables.js.map +1 -1
- package/dist/redact.js +56 -23
- package/dist/redact.js.map +1 -1
- package/dist/release.d.ts +7 -0
- package/dist/release.js +19 -3
- package/dist/release.js.map +1 -1
- package/dist/runtime.d.ts +5 -1
- package/dist/runtime.js +303 -96
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +31 -18
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +11 -4
- package/dist/sessions/anchor.js +33 -37
- 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 +49 -23
- package/dist/sessions/background-tasks.js +161 -89
- 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 +23 -6
- package/dist/sessions/cli-sessions.js +84 -84
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +38 -2
- package/dist/sessions/completions.js +348 -77
- 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 +2 -0
- package/dist/sessions/export.js +6 -2
- package/dist/sessions/export.js.map +1 -1
- package/dist/sessions/gateway-sessions.d.ts +86 -0
- package/dist/sessions/gateway-sessions.js +914 -0
- package/dist/sessions/gateway-sessions.js.map +1 -0
- package/dist/sessions/gateway-watcher.d.ts +10 -11
- package/dist/sessions/gateway-watcher.js +25 -21
- 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/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 +34 -18
- package/dist/sessions/list.js +161 -147
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +77 -13
- package/dist/sessions/merge.js +315 -65
- 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 +85 -0
- package/dist/sessions/prefix-index.js +243 -0
- package/dist/sessions/prefix-index.js.map +1 -0
- package/dist/sessions/recovery.d.ts +4 -7
- package/dist/sessions/recovery.js +100 -283
- package/dist/sessions/recovery.js.map +1 -1
- package/dist/sessions/relay.d.ts +20 -20
- package/dist/sessions/relay.js +205 -127
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +251 -78
- package/dist/sessions/service.js +1510 -564
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +20 -2
- package/dist/sessions/session.js +33 -5
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/shares.d.ts +7 -6
- package/dist/sessions/shares.js +22 -45
- package/dist/sessions/shares.js.map +1 -1
- package/dist/sessions/source-kind.js +1 -0
- package/dist/sessions/source-kind.js.map +1 -1
- package/dist/sessions/state-db-identity.d.ts +18 -0
- package/dist/sessions/state-db-identity.js +67 -0
- package/dist/sessions/state-db-identity.js.map +1 -0
- package/dist/sessions/state-db-worker.js +24 -5
- package/dist/sessions/state-db-worker.js.map +1 -1
- package/dist/sessions/state-db.d.ts +157 -21
- package/dist/sessions/state-db.js +1145 -204
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/state-saved.d.ts +1 -1
- package/dist/sessions/state-saved.js +35 -11
- package/dist/sessions/state-saved.js.map +1 -1
- package/dist/sessions/storage/backend.d.ts +171 -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 +25 -0
- package/dist/sessions/storage/json-documents.js +277 -0
- package/dist/sessions/storage/json-documents.js.map +1 -0
- package/dist/sessions/storage/json-files.d.ts +23 -0
- package/dist/sessions/storage/json-files.js +244 -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 +113 -0
- package/dist/sessions/storage/json-journal.js +756 -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 +82 -0
- package/dist/sessions/storage/json.js +350 -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 +67 -73
- package/dist/sessions/store.js +341 -607
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +12 -7
- package/dist/sessions/streams.js +58 -15
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/titles.js +1 -1
- package/dist/sessions/titles.js.map +1 -1
- package/dist/sessions/todo.d.ts +5 -0
- package/dist/sessions/todo.js +28 -21
- package/dist/sessions/todo.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +8 -5
- package/dist/sessions/tool-display.js +28 -16
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +10 -1
- package/dist/sessions/turn-context.js +13 -6
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +110 -22
- package/dist/sessions/turn.js +918 -380
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/wakeup-pause.d.ts +8 -13
- package/dist/sessions/wakeup-pause.js +31 -49
- package/dist/sessions/wakeup-pause.js.map +1 -1
- package/dist/sessions/window.js +13 -12
- package/dist/sessions/window.js.map +1 -1
- package/dist/settings.d.ts +19 -7
- package/dist/settings.js +179 -183
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/client.d.ts +41 -4
- package/dist/sidecar/client.js +192 -42
- 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 +6 -2
- package/dist/sidecar/fake.js +34 -4
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/spa.d.ts +9 -4
- package/dist/spa.js +36 -12
- package/dist/spa.js.map +1 -1
- package/dist/startup-timing.d.ts +66 -0
- package/dist/startup-timing.js +129 -0
- package/dist/startup-timing.js.map +1 -0
- 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/fs-handles.d.ts +11 -0
- package/dist/test/fs-handles.js +31 -0
- package/dist/test/fs-handles.js.map +1 -0
- package/dist/test/harness.d.ts +1 -0
- package/dist/test/harness.js +1 -1
- package/dist/test/harness.js.map +1 -1
- package/dist/test/passkeys.d.ts +22 -0
- package/dist/test/passkeys.js +62 -0
- package/dist/test/passkeys.js.map +1 -0
- package/dist/test/storage.d.ts +15 -0
- package/dist/test/storage.js +26 -0
- package/dist/test/storage.js.map +1 -0
- package/dist/tools/commands.d.ts +6 -1
- package/dist/tools/commands.js +8 -2
- package/dist/tools/commands.js.map +1 -1
- package/dist/tools/connectors.d.ts +33 -0
- package/dist/tools/connectors.js +121 -0
- package/dist/tools/connectors.js.map +1 -0
- package/dist/tools/crons.d.ts +46 -5
- package/dist/tools/crons.js +100 -26
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +34 -5
- package/dist/tools/dashboard-plugins.js +146 -14
- package/dist/tools/dashboard-plugins.js.map +1 -1
- package/dist/tools/extensions.d.ts +5 -4
- package/dist/tools/extensions.js +213 -207
- package/dist/tools/extensions.js.map +1 -1
- package/dist/tools/gateway-restart.d.ts +82 -0
- package/dist/tools/gateway-restart.js +222 -0
- package/dist/tools/gateway-restart.js.map +1 -0
- package/dist/tools/health.d.ts +28 -4
- package/dist/tools/health.js +171 -50
- 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/insights.d.ts +1 -1
- package/dist/tools/insights.js +2 -2
- package/dist/tools/insights.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-health.js +74 -24
- package/dist/tools/mcp-health.js.map +1 -1
- package/dist/tools/mcp.js +19 -1
- 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-oauth.d.ts +14 -0
- package/dist/tools/plugin-oauth.js +53 -0
- package/dist/tools/plugin-oauth.js.map +1 -0
- 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/prune-advice.d.ts +50 -0
- package/dist/tools/prune-advice.js +114 -0
- package/dist/tools/prune-advice.js.map +1 -0
- package/dist/tools/skills.d.ts +13 -5
- package/dist/tools/skills.js +53 -19
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +102 -17
- package/dist/tools/update-notifications.js +400 -82
- 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 +100 -33
- package/dist/tools/updates.js +1085 -345
- package/dist/tools/updates.js.map +1 -1
- package/dist/util.d.ts +2 -0
- package/dist/util.js +5 -0
- package/dist/util.js.map +1 -1
- package/dist/workspace/git.d.ts +16 -1
- package/dist/workspace/git.js +291 -11
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media.d.ts +12 -9
- package/dist/workspace/media.js +98 -38
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/preview.js +4 -14
- package/dist/workspace/preview.js.map +1 -1
- package/dist/workspace/rollback.d.ts +8 -4
- package/dist/workspace/rollback.js +22 -12
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/workspaces.d.ts +15 -19
- package/dist/workspace/workspaces.js +55 -127
- package/dist/workspace/workspaces.js.map +1 -1
- package/dist/workspace/worktrees.js +170 -32
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +6 -4
- package/sidecar/agent_dependency.json +4 -4
- package/sidecar/agent_support.json +29 -0
- package/sidecar/scripts/record-fixtures.py +12 -3
- package/sidecar/scripts/replay_sidecar.py +8 -0
- package/sidecar/scripts/suite_runner.py +458 -0
- package/sidecar/scripts/test.sh +11 -5
- package/sidecar/talaria_sidecar/__init__.py +6 -1
- package/sidecar/talaria_sidecar/__main__.py +13 -4
- package/sidecar/talaria_sidecar/agent_mutation.py +1111 -0
- package/sidecar/talaria_sidecar/agent_services.py +1116 -0
- package/sidecar/talaria_sidecar/context_detail.py +59 -0
- package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
- package/sidecar/talaria_sidecar/home.py +133 -14
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +211 -41
- package/sidecar/talaria_sidecar/methods/chat.py +1378 -200
- package/sidecar/talaria_sidecar/methods/checkpoints.py +29 -4
- package/sidecar/talaria_sidecar/methods/commands.py +483 -13
- package/sidecar/talaria_sidecar/methods/config.py +195 -6
- package/sidecar/talaria_sidecar/methods/connectors.py +375 -0
- package/sidecar/talaria_sidecar/methods/cron.py +374 -36
- package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
- package/sidecar/talaria_sidecar/methods/dashboard_plugins.py +301 -0
- package/sidecar/talaria_sidecar/methods/gateway.py +215 -55
- package/sidecar/talaria_sidecar/methods/gateway_sessions.py +418 -0
- package/sidecar/talaria_sidecar/methods/goals.py +81 -41
- package/sidecar/talaria_sidecar/methods/kanban.py +82 -30
- package/sidecar/talaria_sidecar/methods/memory.py +84 -0
- package/sidecar/talaria_sidecar/methods/oauth.py +144 -13
- package/sidecar/talaria_sidecar/methods/plugin_catalog.py +198 -0
- package/sidecar/talaria_sidecar/methods/plugin_oauth.py +234 -0
- package/sidecar/talaria_sidecar/methods/process.py +229 -16
- package/sidecar/talaria_sidecar/methods/profiles.py +48 -9
- package/sidecar/talaria_sidecar/methods/providers.py +196 -26
- package/sidecar/talaria_sidecar/methods/runtime.py +71 -1
- package/sidecar/talaria_sidecar/methods/skills.py +83 -5
- package/sidecar/talaria_sidecar/methods/state_db.py +405 -112
- 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 +51 -0
- package/sidecar/talaria_sidecar/methods/usage.py +113 -14
- package/sidecar/talaria_sidecar/methods/worktree.py +145 -19
- package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
- package/sidecar/talaria_sidecar/rpc.py +70 -7
- package/sidecar/talaria_sidecar/runtime.py +122 -14
- package/sidecar/talaria_sidecar/speed.py +47 -0
- package/sidecar/talaria_sidecar/startup_timing.py +119 -0
- package/static/dist/FILES.txt +242 -215
- package/static/dist/THIRD_PARTY_LICENSES.md +6 -22
- package/static/dist/assets/AppShell-3isgvoro.js +8 -0
- package/static/dist/assets/Button-f4n6dapf.js +1 -0
- package/static/dist/assets/ChatPage-k7b6xfax.js +4 -0
- package/static/dist/assets/ConnectTargets-juqvt5lm.js +1 -0
- package/static/dist/assets/Dialog-hta5utr3.js +1 -0
- package/static/dist/assets/ExtensionRoute-zrhjn0wm.js +1 -0
- package/static/dist/assets/Field-b6ez48oz.js +1 -0
- package/static/dist/assets/HubRoute-iwvso2tz.js +2 -0
- package/static/dist/assets/LinkCheckContext-ix6zqqkk.js +1 -0
- package/static/dist/assets/Markdown-jdeth7zq.js +2 -0
- package/static/dist/assets/MarkdownRenderer-fbswsgld.js +302 -0
- package/static/dist/assets/MarkdownRenderer-i9o6nr38.css +1 -0
- package/static/dist/assets/OAuthSignIn-bpd8hom3.js +1 -0
- package/static/dist/assets/ResourceSwitch-odi6dy2o.js +1 -0
- package/static/dist/assets/RightPanel-eu6enlo6.js +1 -0
- package/static/dist/assets/Select-kr07928w.js +1 -0
- package/static/dist/assets/SessionListPanel-etb2i54l.js +1 -0
- package/static/dist/assets/SettingsLayout-eu3xxp23.js +1 -0
- package/static/dist/assets/States-icab19yg.js +1 -0
- package/static/dist/assets/TerminalPanel-hngmsr8b.js +16 -0
- package/static/dist/assets/Toaster-nb2uzjw1.js +1 -0
- package/static/dist/assets/_app-ga1moszl.js +1 -0
- package/static/dist/assets/_app.ext._extensionId-na62087t.js +1 -0
- package/static/dist/assets/_app.index-jypgdqoi.js +1 -0
- package/static/dist/assets/_app.insights-kabejdtm.js +1 -0
- package/static/dist/assets/_app.kanban-m19kfcmx.js +1 -0
- package/static/dist/assets/_app.logs-iw4m96sz.js +1 -0
- package/static/dist/assets/_app.memory-jq5frwcg.js +1 -0
- package/static/dist/assets/_app.profiles-kwa3m7rp.js +1 -0
- package/static/dist/assets/_app.session._sessionId-o5lyeipj.js +1 -0
- package/static/dist/assets/_app.settings-ckl8hdll.js +1 -0
- package/static/dist/assets/_app.settings._section-dtdjk98a.js +3 -0
- package/static/dist/assets/_app.skills-n016rihd.js +1 -0
- package/static/dist/assets/_app.tasks-n8zuqsqe.js +1 -0
- package/static/dist/assets/_app.todos-btew5v2s.js +1 -0
- package/static/dist/assets/_app.workspaces-lnfk95ch.js +1 -0
- package/static/dist/assets/abnfDiagram-VCTEODGH-dttheegb.js +1 -0
- package/static/dist/assets/{angular-html-nu6l31w3.js → angular-html-l28bd5fo.js} +1 -1
- package/static/dist/assets/{angular-ts-fbx4f8mm.js → angular-ts-ffzx1vj0.js} +1 -1
- package/static/dist/assets/{apl-bx2xr7dh.js → apl-b7h7mmy4.js} +1 -1
- package/static/dist/assets/{arc-7popfhbk.js → arc-fk1b21lm.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-ic435919.js +1 -0
- package/static/dist/assets/architectureDiagram-5GKGNRK7-8r3glgiq.js +36 -0
- package/static/dist/assets/archive-il7c6053.js +1 -0
- package/static/dist/assets/{astro-g77i97cb.js → astro-k117fy0t.js} +1 -1
- package/static/dist/assets/back-gu3nu279.js +1 -0
- package/static/dist/assets/{blade-boo0u92y.js → blade-npefsmn8.js} +1 -1
- package/static/dist/assets/{blockDiagram-I7D4REHJ-jzthp1wa.js → blockDiagram-I7D4REHJ-jenyos6c.js} +4 -4
- package/static/dist/assets/{c-cat0w3sc.js → c-2umg6kbn.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-mwr67mt8.js → c4Diagram-7LVT6UL2-nzlx7yc4.js} +1 -1
- package/static/dist/assets/cancel-bsnfo36u.js +1 -0
- package/static/dist/assets/channel-dsbtk01y.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-gpq1y228.js → chunk-4HAMMTFA-g0cru63b.js} +20 -20
- package/static/dist/assets/{chunk-75Z2AOVW-ktje58c4.js → chunk-75Z2AOVW-7rkswkjw.js} +1 -1
- package/static/dist/assets/chunk-DU6HZSFF-fn73jzdo.js +128 -0
- package/static/dist/assets/{chunk-F27PBJKO-cmev85ti.js → chunk-F27PBJKO-oey8e72m.js} +1 -1
- package/static/dist/assets/{chunk-FOHPRMQF-opuflp73.js → chunk-FOHPRMQF-ipewusgj.js} +8 -8
- package/static/dist/assets/{chunk-GMAD6QVW-htjgt22y.js → chunk-GMAD6QVW-l2a32wiy.js} +6 -6
- package/static/dist/assets/{chunk-GVQU2GXP-qndvoxl7.js → chunk-GVQU2GXP-o9dwzh8h.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-e5bkd5q6.js → chunk-IMKFNOWR-eblwj20b.js} +5 -5
- package/static/dist/assets/{chunk-L3NEJ4N5-mj1155r6.js → chunk-L3NEJ4N5-o2mufspb.js} +1 -1
- package/static/dist/assets/chunk-OSK3NFVY-ijp9pw29.js +10 -0
- package/static/dist/assets/{chunk-P2QGCYS3-f7t54xfm.js → chunk-P2QGCYS3-hs9hqwl4.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-i927v9oa.js → chunk-POPQ4Y6H-bp9rsqez.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-m84lmnyt.js → chunk-PWAF6VOD-brkprov6.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-mi0tlcu9.js → chunk-SHT3W25Y-gmpv0pxz.js} +13 -13
- package/static/dist/assets/{chunk-SVP7TREG-dnau702j.js → chunk-SVP7TREG-oldm08fu.js} +4 -4
- package/static/dist/assets/{chunk-TICWLB2K-edem1gkg.js → chunk-TICWLB2K-ooqilfv7.js} +6 -6
- package/static/dist/assets/{chunk-XXDRQBXY-n40pf9vy.js → chunk-XXDRQBXY-jkmnqu65.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-fvgcv24g.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-fvgcv24g.js +1 -0
- package/static/dist/assets/{cobol-go622aep.js → cobol-ssf9hkh7.js} +1 -1
- package/static/dist/assets/{coffee-jwo1xjhq.js → coffee-lsi9fwae.js} +1 -1
- package/static/dist/assets/copied-bz3gkazh.js +1 -0
- package/static/dist/assets/copy-jo006coq.js +1 -0
- package/static/dist/assets/cose-bilkent-JH36ORCC-layy2r27.js +1 -0
- package/static/dist/assets/{cpp-nre7j0kq.js → cpp-lfrh4y55.js} +1 -1
- package/static/dist/assets/{crystal-lviso2je.js → crystal-efh4pte7.js} +1 -1
- package/static/dist/assets/{css-ywz9j4hb.js → css-i82qdo89.js} +1 -1
- package/static/dist/assets/cynefin-OW5HDTMX-d47op7qs.js +1 -0
- package/static/dist/assets/cynefinDiagram-5FMLGOSQ-doa7594y.js +62 -0
- package/static/dist/assets/cytoscape.esm-fjn2algp.js +321 -0
- package/static/dist/assets/dagre-GXQ25YYZ-os768dp4.js +4 -0
- package/static/dist/assets/dagre-o5jmjz46.js +1 -0
- package/static/dist/assets/{diagram-S7CK7UJ4-l8abxaes.js → diagram-S7CK7UJ4-fyi81nxd.js} +4 -4
- package/static/dist/assets/diagram-UQ7AKVKN-j3exggaq.js +41 -0
- package/static/dist/assets/{diagram-VSXAHHWV-fnky6lvc.js → diagram-VSXAHHWV-omo1en2l.js} +1 -1
- package/static/dist/assets/diagram-VX7I27RA-oro2cyu0.js +24 -0
- package/static/dist/assets/diagram-Z3DM3KII-byt9vdb7.js +24 -0
- package/static/dist/assets/dist-crc46di9.js +7 -0
- package/static/dist/assets/dist-etpoyscq.js +44 -0
- package/static/dist/assets/{dist-c4c2m3kv.js → dist-nb670eu6.js} +1 -1
- package/static/dist/assets/ebnfDiagram-PWID7BFC-ee3haugh.js +1 -0
- package/static/dist/assets/{edge-dvv35byi.js → edge-lrob635n.js} +1 -1
- package/static/dist/assets/{elixir-i2f1ki1g.js → elixir-kdpxeq93.js} +1 -1
- package/static/dist/assets/{elm-cpepyjm4.js → elm-c8irj9uh.js} +1 -1
- package/static/dist/assets/erDiagram-RLTQ6QDP-d1e93yff.js +99 -0
- package/static/dist/assets/{erb-idlyfheg.js → erb-flwsv146.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-i1ihb40c.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-jahfpiod.js +1 -0
- package/static/dist/assets/folder-plus-ivoglq98.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-ngbsyp37.js → ganttDiagram-EL5Y4UJY-m7g0jud4.js} +1 -1
- package/static/dist/assets/{git-rebase-d9lpfpbn.js → git-rebase-bli19aoq.js} +1 -1
- package/static/dist/assets/gitGraph-4MIJSDKK-hzdj07yy.js +1 -0
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-eravdti4.js → gitGraphDiagram-WWUBYQGX-dm9agbna.js} +1 -1
- package/static/dist/assets/{glimmer-js-39i0urfb.js → glimmer-js-d4icg38p.js} +1 -1
- package/static/dist/assets/{glimmer-ts-gdtd6n73.js → glimmer-ts-bk75772u.js} +1 -1
- package/static/dist/assets/{glsl-o1djm5pp.js → glsl-lqormp2b.js} +1 -1
- package/static/dist/assets/{graphql-kj6rz55t.js → graphql-hw6s5qzd.js} +1 -1
- package/static/dist/assets/{hack-pe1qqr2i.js → hack-ozrv6l4d.js} +1 -1
- package/static/dist/assets/{haml-cknnbcqs.js → haml-gytfosdt.js} +1 -1
- package/static/dist/assets/{handlebars-hwq64orh.js → handlebars-b6g7jffd.js} +1 -1
- package/static/dist/assets/highlighted-body-KPVGNVTW-mmgz2czu.js +1 -0
- package/static/dist/assets/host-fr6lni1p.js +1 -0
- package/static/dist/assets/{html-derivative-h2i2pyjc.js → html-derivative-f1tqcroo.js} +1 -1
- package/static/dist/assets/{html-ha05xtvt.js → html-on6oj6yj.js} +1 -1
- package/static/dist/assets/{http-cf94zu27.js → http-odhufsl7.js} +1 -1
- package/static/dist/assets/{hurl-lslax568.js → hurl-mu680336.js} +1 -1
- package/static/dist/assets/index-73649w7o.js +10 -0
- package/static/dist/assets/index-lhf1fsdf.css +2 -0
- package/static/dist/assets/info-A6RAGUB7-lll1dqmm.js +1 -0
- package/static/dist/assets/infoDiagram-27XIBGKW-k7esc639.js +2 -0
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-i6ypq351.js → ishikawaDiagram-5VMMS53U-lgr6k9wo.js} +6 -6
- package/static/dist/assets/{java-ndjf2o89.js → java-nxbe8uju.js} +1 -1
- package/static/dist/assets/{javascript-coftlwy3.js → javascript-lz3kxcyz.js} +1 -1
- package/static/dist/assets/{jinja-etei9wy7.js → jinja-l5pk84gt.js} +1 -1
- package/static/dist/assets/{jison-hyekgft1.js → jison-jdutdwyy.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-lduf1z95.js → journeyDiagram-3NMN7TZE-ndmbxuqk.js} +1 -1
- package/static/dist/assets/{json-lok0yaho.js → json-dnge4ssf.js} +1 -1
- package/static/dist/assets/{jsx-b64w0nbx.js → jsx-c3lwhkep.js} +1 -1
- package/static/dist/assets/{jsx-runtime-eu92a5pu.js → jsx-runtime-o40ukmjl.js} +1 -1
- package/static/dist/assets/{julia-jxxzz2yn.js → julia-hwd4rrcb.js} +1 -1
- package/static/dist/assets/{just-mc29iw9t.js → just-nka3oxdx.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-e18bu4pg.js → kanban-definition-UXKFOSKX-mjxym0jp.js} +28 -28
- package/static/dist/assets/katex-cbdd1ohw.css +1 -0
- package/static/dist/assets/katex-pbij2oo6.js +257 -0
- package/static/dist/assets/{latex-pafa03ft.js → latex-iqmr7e40.js} +1 -1
- package/static/dist/assets/lazyRouteComponent-cwb6l7xh.js +1 -0
- package/static/dist/assets/{line-meizqsvn.js → line-mtr5k6il.js} +1 -1
- package/static/dist/assets/linear-jlwrvvy6.js +1 -0
- package/static/dist/assets/link-iadw4ghw.js +2 -0
- package/static/dist/assets/{liquid-hz5c2pov.js → liquid-okmk9tbp.js} +1 -1
- package/static/dist/assets/locale-cs-n41hptse.js +5 -0
- package/static/dist/assets/locale-de-k7ht47ew.js +5 -0
- package/static/dist/assets/locale-es-ih078lco.js +5 -0
- package/static/dist/assets/locale-fr-ojabnf41.js +5 -0
- package/static/dist/assets/locale-it-kb96jufy.js +5 -0
- package/static/dist/assets/locale-ja-jpsp3xs1.js +7 -0
- package/static/dist/assets/locale-ko-cpzuy0n7.js +5 -0
- package/static/dist/assets/locale-pl-dbap4leu.js +5 -0
- package/static/dist/assets/locale-pt-nci7w72x.js +5 -0
- package/static/dist/assets/locale-ru-soghe4cs.js +7 -0
- package/static/dist/assets/locale-tr-ks00mu68.js +5 -0
- package/static/dist/assets/locale-vi-ij7twfm6.js +5 -0
- package/static/dist/assets/locale-zh-Hant-feakwr7a.js +5 -0
- package/static/dist/assets/locale-zh-ren0vjyv.js +7 -0
- package/static/dist/assets/login-lkhtzfcl.js +1 -0
- package/static/dist/assets/{lua-leyf8mlb.js → lua-krx4xj5h.js} +1 -1
- package/static/dist/assets/{marko-cn1fzpdv.js → marko-gbvpfehz.js} +1 -1
- package/static/dist/assets/{mdc-huhy88df.js → mdc-c21zlyi5.js} +1 -1
- package/static/dist/assets/mermaid-HWGCJPDP-jx0q5dm2.js +1 -0
- package/static/dist/assets/mermaid-parser.core-k91vdnvi.js +7 -0
- package/static/dist/assets/micromark-factory-space-fnmnpi6s.js +1 -0
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-27b26eok.js → mindmap-definition-YA3MSWOX-ehztfea4.js} +28 -28
- package/static/dist/assets/{nginx-e7lvhsmn.js → nginx-f3d68ed4.js} +1 -1
- package/static/dist/assets/{nim-dun5d8mk.js → nim-dw0jcy70.js} +1 -1
- package/static/dist/assets/onboarding-f67z0a2r.js +1 -0
- package/static/dist/assets/packet-AYTQ26CC-ok2xrblz.js +1 -0
- package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
- package/static/dist/assets/pegDiagram-XKGWAZYB-ec3rpj0m.js +1 -0
- package/static/dist/assets/{perl-bj3oqtet.js → perl-3c0r45c8.js} +1 -1
- package/static/dist/assets/{php-buzp12r0.js → php-rznhidvp.js} +1 -1
- package/static/dist/assets/pie-WAS4IAKB-iwkir23u.js +1 -0
- package/static/dist/assets/pieDiagram-E7YTZNPT-fcpqpekh.js +39 -0
- package/static/dist/assets/{pug-mjad7he9.js → pug-f2le4f3y.js} +1 -1
- package/static/dist/assets/{qml-d1gdp10s.js → qml-e2wbq9cr.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-hhue7d8o.js → quadrantDiagram-AXDQQJYC-lqsync43.js} +1 -1
- package/static/dist/assets/queryKeys-ed4n2egc.js +9 -0
- package/static/dist/assets/{r-o3r9c7ao.js → r-epfzuw6q.js} +1 -1
- package/static/dist/assets/radar-RG4KPBEZ-hje4jbk6.js +1 -0
- package/static/dist/assets/railroad-74A4TZTK-gll9n6qi.js +1 -0
- package/static/dist/assets/railroad-abnf-HS5TGJTU-o6h6lpn4.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-juaefj6o.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-cgfhvpah.js +1 -0
- package/static/dist/assets/railroadDiagram-O6MQD6OU-lp3w7awf.js +1 -0
- package/static/dist/assets/{razor-ilc585yr.js → razor-pea1u1d0.js} +1 -1
- package/static/dist/assets/refresh-cw-iccm7a4e.js +1 -0
- package/static/dist/assets/refresh-i9ya9dvl.js +1 -0
- package/static/dist/assets/{regexp-mpjtcvwm.js → regexp-hzte0a5x.js} +1 -1
- package/static/dist/assets/registry-g7w3353u.js +1 -0
- package/static/dist/assets/requirementDiagram-BXWQKSXE-ha8eibcf.js +84 -0
- package/static/dist/assets/rolldown-runtime-bnnw9q4a.js +1 -0
- package/static/dist/assets/{rst-jrkwjdyi.js → rst-m6tugj5o.js} +1 -1
- package/static/dist/assets/{ruby-n4tjcbpz.js → ruby-l19vq3w1.js} +1 -1
- package/static/dist/assets/runtime-nlxpnegr.js +1 -0
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-fqb8xcjf.js → sankeyDiagram-P5KCCOFB-oxf31pr9.js} +1 -1
- package/static/dist/assets/{sas-hjo6y93k.js → sas-ktdxo397.js} +1 -1
- package/static/dist/assets/{scss-ncidjv48.js → scss-kn3oxfwf.js} +1 -1
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-dlcz6qjz.js → sequenceDiagram-WJ2MYXX4-jiq8lhyo.js} +11 -11
- package/static/dist/assets/settings_section_extensions_title-fadc7o65.js +1 -0
- package/static/dist/assets/share._token-o76ki0ly.js +2 -0
- package/static/dist/assets/{shellscript-m8whjpqd.js → shellscript-l100jtrg.js} +1 -1
- package/static/dist/assets/{shellsession-i5bj6syz.js → shellsession-m5qukwow.js} +1 -1
- package/static/dist/assets/sizeCapture-INFHLROL-dk1io13w.js +1 -0
- package/static/dist/assets/sliders-horizontal-ddcs88av.js +1 -0
- package/static/dist/assets/{soy-hhh36ysq.js → soy-mexlscur.js} +1 -1
- package/static/dist/assets/{sql-j21bzcl6.js → sql-o0ho5suh.js} +1 -1
- package/static/dist/assets/src-h89s0hs4.js +1 -0
- package/static/dist/assets/{stata-bbu2onh1.js → stata-be9ck1q3.js} +1 -1
- package/static/dist/assets/stateDiagram-D77RDMKH-f3fslnon.js +1 -0
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hlt6yphw.js +1 -0
- package/static/dist/assets/{surrealql-l5vczuco.js → surrealql-gk1xkkyy.js} +1 -1
- package/static/dist/assets/{svelte-idn422g6.js → svelte-k6ecjilf.js} +1 -1
- package/static/dist/assets/swimlanes-42K2YHIH-k1hmk7jf.js +1 -0
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-hd5t9tnt.js +8 -0
- package/static/dist/assets/{templ-e22qou3c.js → templ-gf3bftk6.js} +1 -1
- package/static/dist/assets/{tex-deqpc5ee.js → tex-d21fqv1z.js} +1 -1
- package/static/dist/assets/{timeline-definition-24CTP7MA-b1vl2zil.js → timeline-definition-24CTP7MA-n3ncfy9r.js} +8 -8
- package/static/dist/assets/toast-liparz2j.js +56 -0
- package/static/dist/assets/treeView-Q6P3EWNA-l3y2pejt.js +1 -0
- package/static/dist/assets/treemap-WGGIJYW6-jown9axh.js +1 -0
- package/static/dist/assets/{ts-tags-ctnibc27.js → ts-tags-n0g61gr6.js} +1 -1
- package/static/dist/assets/{tsx-i163n32g.js → tsx-cvv7wv6q.js} +1 -1
- package/static/dist/assets/{twig-x0fi7wm8.js → twig-n1f67204.js} +1 -1
- package/static/dist/assets/{typescript-g18c34xz.js → typescript-og7o55cx.js} +1 -1
- package/static/dist/assets/untitled-mj86r4gz.js +1 -0
- package/static/dist/assets/useDebounced-g8r0ggve.js +1 -0
- package/static/dist/assets/useForm-o4uu74mx.js +1 -0
- package/static/dist/assets/{useParams-b4dq6n4q.js → useParams-ftsveu84.js} +1 -1
- package/static/dist/assets/{useSearch-ipfmv3he.js → useSearch-crqueqga.js} +1 -1
- package/static/dist/assets/vennDiagram-4TSXK5OY-dh59uq1c.js +34 -0
- package/static/dist/assets/{vue-mwu5l1bb.js → vue-ej4xpwqw.js} +1 -1
- package/static/dist/assets/{vue-html-nskj3du0.js → vue-html-d48xsjuo.js} +1 -1
- package/static/dist/assets/{vue-vine-cqe77smq.js → vue-vine-pev88ip6.js} +1 -1
- package/static/dist/assets/wardley-WFR3VGLG-e6d74b0j.js +1 -0
- package/static/dist/assets/wardleyDiagram-VM6X3IG4-hnofh78f.js +78 -0
- package/static/dist/assets/with-selector-nu1b5z0v.js +1 -0
- package/static/dist/assets/{xml-gau7gvrv.js → xml-ifxp6gqb.js} +1 -1
- package/static/dist/assets/{xsl-ew09zq8b.js → xsl-fug2cckc.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-e49523ab.js → xychartDiagram-S5SC5T6Z-bpymjelb.js} +6 -6
- package/static/dist/assets/{yaml-ofnrxtvf.js → yaml-fjmafnlo.js} +1 -1
- package/static/dist/extension-sdk.js +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +180 -136
- package/static/dist/assets/AppShell-g3bkgnvt.js +0 -1
- package/static/dist/assets/ChatPage-oo7bdan6.js +0 -25
- package/static/dist/assets/Dialog-g355qxhp.js +0 -1
- package/static/dist/assets/ExtensionRoute-i2y9fvjm.js +0 -1
- package/static/dist/assets/HubRoute-lmkrbw47.js +0 -2
- package/static/dist/assets/Markdown-j4kut4j2.js +0 -598
- package/static/dist/assets/Markdown-lwviulmg.css +0 -1
- package/static/dist/assets/RightPanel-ohlsdzfl.js +0 -1
- package/static/dist/assets/Select-kddq75qc.js +0 -1
- package/static/dist/assets/SessionListPanel-lu3oy0hw.js +0 -1
- package/static/dist/assets/SettingsLayout-o9bv4mgb.js +0 -1
- package/static/dist/assets/States-eq6fe29m.js +0 -1
- package/static/dist/assets/Toaster-pc247u9f.js +0 -1
- package/static/dist/assets/_app-f8n1i0m1.js +0 -1
- package/static/dist/assets/_app.ext._extensionId-gfbzpoou.js +0 -1
- package/static/dist/assets/_app.index-bzzv80hk.js +0 -1
- package/static/dist/assets/_app.insights-dzixbryi.js +0 -1
- package/static/dist/assets/_app.kanban-c6hep2us.js +0 -1
- package/static/dist/assets/_app.logs-i69qds98.js +0 -1
- package/static/dist/assets/_app.memory-kl4vf0gd.js +0 -1
- package/static/dist/assets/_app.profiles-b8zinmyp.js +0 -1
- package/static/dist/assets/_app.session._sessionId-fn1bhde6.js +0 -1
- package/static/dist/assets/_app.settings-cg8plwmf.js +0 -1
- package/static/dist/assets/_app.settings._section-bkwbi9zn.js +0 -3
- package/static/dist/assets/_app.skills-cs47sbvs.js +0 -1
- package/static/dist/assets/_app.tasks-dwvk64tc.js +0 -1
- package/static/dist/assets/_app.todos-c8xjm8p3.js +0 -1
- package/static/dist/assets/_app.workspaces-ehvd1gds.js +0 -1
- package/static/dist/assets/abnfDiagram-VCTEODGH-c8kdixke.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-6crvnylg.js +0 -1
- package/static/dist/assets/architectureDiagram-5GKGNRK7-cku06x1p.js +0 -36
- package/static/dist/assets/back-h7aun7ew.js +0 -1
- package/static/dist/assets/cancel-fhi0vcnu.js +0 -1
- package/static/dist/assets/channel-b2zo73z1.js +0 -1
- package/static/dist/assets/chunk-DU6HZSFF-ofajdixh.js +0 -128
- package/static/dist/assets/chunk-OSK3NFVY-e7gkikcw.js +0 -10
- package/static/dist/assets/classDiagram-ZZMXUADV-oe99st8x.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-oe99st8x.js +0 -1
- package/static/dist/assets/copied-hcplv46p.js +0 -1
- package/static/dist/assets/copy-magtofyp.js +0 -1
- package/static/dist/assets/cose-bilkent-JH36ORCC-eclcy6ja.js +0 -1
- package/static/dist/assets/cynefin-OW5HDTMX-siqlulkl.js +0 -1
- package/static/dist/assets/cynefinDiagram-5FMLGOSQ-i648tah9.js +0 -62
- package/static/dist/assets/cytoscape.esm-bqu53u12.js +0 -321
- package/static/dist/assets/dagre-GXQ25YYZ-mwn6uc17.js +0 -4
- package/static/dist/assets/dagre-lkoknbjn.js +0 -1
- package/static/dist/assets/diagram-UQ7AKVKN-mekq7zgq.js +0 -41
- package/static/dist/assets/diagram-VX7I27RA-xe6r5qfw.js +0 -24
- package/static/dist/assets/diagram-Z3DM3KII-nq07dg7k.js +0 -24
- package/static/dist/assets/ebnfDiagram-PWID7BFC-c62ipm3g.js +0 -1
- package/static/dist/assets/endpoints-klbxod4t.js +0 -64
- package/static/dist/assets/erDiagram-RLTQ6QDP-h39kmnzy.js +0 -99
- package/static/dist/assets/eventmodeling-NTZA5JFV-obfede4u.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-kdpsp1je.js +0 -1
- package/static/dist/assets/gitGraph-4MIJSDKK-fr2i7caa.js +0 -1
- package/static/dist/assets/highlighted-body-KPVGNVTW-jwpop9p8.js +0 -1
- package/static/dist/assets/host-i828xuo7.js +0 -1
- package/static/dist/assets/index-eh8zilnv.css +0 -2
- package/static/dist/assets/index-r648f4o1.js +0 -11
- package/static/dist/assets/info-A6RAGUB7-h11x0dxo.js +0 -1
- package/static/dist/assets/infoDiagram-27XIBGKW-dfh47dw9.js +0 -2
- package/static/dist/assets/lazyRouteComponent-e1y4t2i9.js +0 -1
- package/static/dist/assets/linear-becwlnhk.js +0 -1
- package/static/dist/assets/link-oot2yn73.js +0 -1
- package/static/dist/assets/login-gu28l9ou.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-mtwrj8fn.js +0 -1
- package/static/dist/assets/mermaid-parser.core-ntne45i9.js +0 -7
- package/static/dist/assets/not-found-ccnnac39.js +0 -1
- package/static/dist/assets/onboarding-lkphzu1s.js +0 -1
- package/static/dist/assets/onboarding_model_label-ftpjdwqk.js +0 -1
- package/static/dist/assets/packet-AYTQ26CC-l6jdka8p.js +0 -1
- package/static/dist/assets/pegDiagram-XKGWAZYB-g1er3bxj.js +0 -1
- package/static/dist/assets/pie-WAS4IAKB-i0ylcrt9.js +0 -1
- package/static/dist/assets/pieDiagram-E7YTZNPT-fbz1e55z.js +0 -39
- package/static/dist/assets/radar-RG4KPBEZ-f9b70aa4.js +0 -1
- package/static/dist/assets/railroad-74A4TZTK-lgsuoeda.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nf9d6pdx.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-f7ummkt5.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-oczq5tqs.js +0 -1
- package/static/dist/assets/railroadDiagram-O6MQD6OU-eqpy9fnz.js +0 -1
- package/static/dist/assets/refresh-abtu93ke.js +0 -1
- package/static/dist/assets/refresh-cw-hlwb02qv.js +0 -1
- package/static/dist/assets/requirementDiagram-BXWQKSXE-ogf6puhi.js +0 -84
- package/static/dist/assets/rolldown-runtime-b9miacsj.js +0 -1
- package/static/dist/assets/root-mbklpuv4.js +0 -1
- package/static/dist/assets/saved-girnabkf.js +0 -1
- package/static/dist/assets/settings_section_extensions_title-h14cwk6f.js +0 -1
- package/static/dist/assets/share._token-kl0r7ido.js +0 -2
- package/static/dist/assets/sizeCapture-INFHLROL-g612l48j.js +0 -1
- package/static/dist/assets/sliders-horizontal-n1zm34qq.js +0 -1
- package/static/dist/assets/src-hjyib4mu.js +0 -1
- package/static/dist/assets/sse-iwku0sjo.js +0 -1
- package/static/dist/assets/stateDiagram-D77RDMKH-fyoznpcs.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-ixhx8r46.js +0 -1
- package/static/dist/assets/swimlanes-42K2YHIH-oefldhfy.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-h9ersk2e.js +0 -8
- package/static/dist/assets/todoStore-jjr5yr78.js +0 -1
- package/static/dist/assets/treeView-Q6P3EWNA-f7hcci5o.js +0 -1
- package/static/dist/assets/treemap-WGGIJYW6-nl85071a.js +0 -1
- package/static/dist/assets/units-kgxzxfhi.js +0 -1
- package/static/dist/assets/untitled-cikfjntn.js +0 -1
- package/static/dist/assets/useForm-kqoqg0k9.js +0 -1
- package/static/dist/assets/useLocale-o59tbiea.js +0 -1
- package/static/dist/assets/vennDiagram-4TSXK5OY-h5hzbk0n.js +0 -34
- package/static/dist/assets/wardley-WFR3VGLG-jftvpm3e.js +0 -1
- package/static/dist/assets/wardleyDiagram-VM6X3IG4-hedh85ge.js +0 -78
- /package/static/dist/assets/{ChatPage-gmu5c7lf.css → TerminalPanel-gmu5c7lf.css} +0 -0
package/dist/sessions/turn.js
CHANGED
|
@@ -5,40 +5,45 @@
|
|
|
5
5
|
* (Python `api/routes.py::_start_chat_stream_for_session`,
|
|
6
6
|
* `api/streaming.py::_run_agent_streaming`, `cancel_stream`).
|
|
7
7
|
*/
|
|
8
|
+
import { StartupTiming, timingOutcome } from '../startup-timing.js';
|
|
8
9
|
import { randomUUID } from 'node:crypto';
|
|
9
|
-
import { closeSync, fstatSync, readFileSync
|
|
10
|
+
import { closeSync, fstatSync, readFileSync } from 'node:fs';
|
|
10
11
|
import { openAnchoredFd } from '../workspace/fs.js';
|
|
11
12
|
import { isWithin, resolvePathLikePython } from '../workspace/paths.js';
|
|
12
|
-
import { SidecarError } from '../sidecar/client.js';
|
|
13
|
+
import { runtimeFailureDetails, SidecarError } from '../sidecar/client.js';
|
|
13
14
|
import { HttpFailure, markSessionTitleGenerated } from './service.js';
|
|
14
15
|
import { StreamRegistry, SessionChannels } from './streams.js';
|
|
15
|
-
import { ChatUsageSchema } from '@maudecode/talaria-web-contracts';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
16
|
+
import { TRANSCRIPT_WINDOW, ChatUsageSchema, ConnectorOperationSnapshotSchema } from '@maudecode/talaria-web-contracts';
|
|
17
|
+
import { toolProgress } from './tool-display.js';
|
|
18
|
+
import { catalogNames, connectorOperation } from '../tools/connectors.js';
|
|
19
|
+
import { PendingPrompts, clarifyReply, isVaultPrompt, vaultReply } from './pending.js';
|
|
20
|
+
import { COMPRESSION_RECOVERY_ACTION_START_FOCUSED, COMPRESSION_RECOVERY_TERMINAL_STATE, CONTEXT_USAGE_FIELDS, Session, contextUsage, forgetContextFigures, titleFrom } from './session.js';
|
|
21
|
+
import { anchorMessageKey, markerSummary, visibleMessagesForAnchor } from './compress.js';
|
|
22
|
+
import { buildActiveTurnToken, completedToolIndex, publicToolFrame, redactNestedMessageContainers, redactString, withToolId } from '../redact.js';
|
|
23
|
+
import { dict, findPersonality } from '../config/agent-config.js';
|
|
22
24
|
import { ReasoningTitleTracker, reasoningEventPayload } from './reasoning-titles.js';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
25
|
+
import { UNSETTLED_TODO_KEY, parseTodoToolResult } from './todo.js';
|
|
26
|
+
import { SessionNotices } from './notices.js';
|
|
27
|
+
import { SessionGoals } from './goals.js';
|
|
28
|
+
import { turnTerminalState } from './anchor.js';
|
|
27
29
|
import { persistentStateChanges, persistentStateSnapshot } from './state-saved.js';
|
|
28
|
-
import {
|
|
29
|
-
import { agentSteerText, assistantReplyAddedAfterCurrentTurn, attachedFilesPrompt, buildPartialMessage, dedupeContext, checkpointTurnStart, extractToolCallsFromMessages, hasNativeImages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, sessionLacksFinalAssistantAnswer, splitDisplayText, stateDbSeenId, joinReasoning, reasoningFieldsText, toolOutcome,
|
|
30
|
-
import { mayBecomeSilentReply, turnOrigin,
|
|
30
|
+
import { agentBuildParams, webuiEphemeralSystemPrompt, workspaceSystemMessage } from './turn-context.js';
|
|
31
|
+
import { agentSteerText, assistantReplyAddedAfterCurrentTurn, attachedFilesPrompt, buildPartialMessage, dedupeContext, checkpointTurnStart, extractToolCallsFromMessages, hasNativeImages, injectMaxIterationSummaryFallback, isDict, mergeDisplayMessagesAfterAgentResult, messageIdentity, messageText, pendingUserRow, sanitizeMessagesForApi, withDataUseRequirements, sessionLacksFinalAssistantAnswer, splitDisplayText, stateDbSeenId, joinReasoning, reasoningFieldsText, toolOutcome, withoutMaxIterationSummaryRequest, withNativeImagesRestored, withoutNativeImages, withoutToolImages } from './merge.js';
|
|
32
|
+
import { mayBecomeSilentReply, turnOrigin, recordBackgroundReview, recordBackgroundUpdate } from './background-updates.js';
|
|
31
33
|
import { countExchanges, fallbackTitleFromExchange, firstExchangeSnippets, isGenericFallbackTitle, latestExchangeSnippets, looksInvalidGeneratedTitle, looksLikeDefaultCliTitle, sanitizeGeneratedTitle, titleLanguageMismatch, titlePrompts } from './titles.js';
|
|
32
34
|
import { WORKSPACE_BUSY_MESSAGE } from '../workspace/git.js';
|
|
33
35
|
import { CHECKPOINT_STORE_BUSY_MESSAGE } from '../workspace/rollback.js';
|
|
34
36
|
import { str } from '../util.js';
|
|
35
37
|
import { toolEditDiff } from './tool-display.js';
|
|
38
|
+
import { startsBackgroundWork } from './background-tasks.js';
|
|
36
39
|
import { recordWakeupPause } from './wakeup-pause.js';
|
|
40
|
+
import { displayName } from '../providers/catalog.js';
|
|
37
41
|
export const CHAT_LOCK_WAIT_SECONDS = 2;
|
|
38
42
|
const IMAGE_MODE_TIMEOUT_MS = 15_000;
|
|
39
|
-
const TERMINAL_SSE_VISIBLE_MESSAGE_LIMIT = 80;
|
|
40
43
|
/** TAL-397: streamed text reports the live meter at most this often (seconds); a counter change reports at once. */
|
|
41
44
|
const METERING_INTERVAL_S = 1;
|
|
45
|
+
/** 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. */
|
|
46
|
+
const CONTENT_EVENTS = new Set(['token', 'reasoning', 'interim_assistant', 'tool', 'tool_complete']);
|
|
42
47
|
/** How long a user's message waits for a stopped background turn to unwind before its own turn is admitted. */
|
|
43
48
|
const BACKGROUND_UNWIND_WAIT_MS = 30_000;
|
|
44
49
|
const COMPRESSION_EXHAUSTED = { label: 'Context compression exhausted', type: 'compression_exhausted', hint: 'The conversation context is too large to compress safely. Start a new conversation or retry with a narrower task.' };
|
|
@@ -65,6 +70,9 @@ export function classifyProviderError(errStr, opts = {}) {
|
|
|
65
70
|
const lower = errStr.toLowerCase();
|
|
66
71
|
if (opts.compressionExhausted)
|
|
67
72
|
return COMPRESSION_EXHAUSTED;
|
|
73
|
+
// TAL-798: the Agent's local llama.cpp provider has no key to add; its server is off or still starting.
|
|
74
|
+
if (opts.condition === 'local_runtime_unavailable')
|
|
75
|
+
return { label: 'Local model server unavailable', type: 'local_runtime_unavailable', hint: 'The local model server is off or still starting. Turn it on in Hermes Agent, or pick another model.' };
|
|
68
76
|
if (opts.condition === 'credential_missing')
|
|
69
77
|
return { label: 'Authentication failed', type: 'auth_mismatch', hint: 'The selected model may not be supported by your configured provider or your API key is invalid. Run `hermes model` in your terminal to update credentials, then restart the WebUI.' };
|
|
70
78
|
const cancelled = ['cancelled by user', 'canceled by user', 'user cancelled', 'user canceled', 'task cancelled', 'task canceled', 'cancellederror'].some((k) => lower.includes(k));
|
|
@@ -209,6 +217,10 @@ export class TurnRunner {
|
|
|
209
217
|
liveUsage = new Map();
|
|
210
218
|
/** TAL-397: each running turn's latest todo list, which an error or cancel keeps on the session. */
|
|
211
219
|
liveTodos = new Map();
|
|
220
|
+
/** The current presentation, scoped to its owning stream and released on every terminal path. */
|
|
221
|
+
runStatuses = new Map();
|
|
222
|
+
/** TAL-1006: each live stream's open compression span, by its start (epoch seconds). */
|
|
223
|
+
compressionSpans = new Map();
|
|
212
224
|
/** Streams whose run completed (`done` emitted) and only await title work; a late cancel is a no-op for these. */
|
|
213
225
|
settledStreams = new Set();
|
|
214
226
|
/** The last adaptive title refresh per session; a successor turn's refresh runs after it (TAL-589). */
|
|
@@ -216,20 +228,147 @@ export class TurnRunner {
|
|
|
216
228
|
/**
|
|
217
229
|
* TAL-364: what a Stop needs to write the turn's model context: its starting context, the prompt the Agent got, and
|
|
218
230
|
* whether a settlement already used a canonical checkpoint (which marks the stop boundary; later work never replaces it).
|
|
231
|
+
* TAL-1162: `startReadPending` while the turn's starting state.db read is still in flight.
|
|
219
232
|
*/
|
|
220
233
|
/** TAL-364: a Stop's in-flight `chat.interrupt` reply (null when it failed), so a worker that settles first can use its checkpoint. */
|
|
221
234
|
interrupts = new Map();
|
|
222
235
|
stopContexts = new Map();
|
|
223
236
|
/** Each turn's worker, so a stopped background turn can be awaited before the user's turn takes the session. */
|
|
224
237
|
workers = new Map();
|
|
238
|
+
/** TAL-899: sessions whose user pressed Stop; no automatic turn starts in them until the user's next turn is admitted. */
|
|
239
|
+
stopHeld = new Set();
|
|
240
|
+
notices = new SessionNotices(() => this.deps.now());
|
|
241
|
+
goals = new SessionGoals(() => this.deps.now());
|
|
225
242
|
constructor(deps) {
|
|
226
243
|
this.deps = deps;
|
|
227
244
|
}
|
|
228
245
|
get registry() { return this.deps.registry; }
|
|
229
246
|
// ── admission ────────────────────────────────────────────────────────────
|
|
247
|
+
/** TAL-899: whether a Stop holds the session's automatic turns (session `automatic_turns_held`). */
|
|
248
|
+
async automaticTurnsHeld(sessionId) {
|
|
249
|
+
if (this.stopHeld.has(sessionId))
|
|
250
|
+
return true;
|
|
251
|
+
try {
|
|
252
|
+
return (await this.deps.store.get(sessionId, { metadataOnly: true })).automatic_turns_stop_held;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/** TAL-803: the Agent's active notices for the session (session `notices`). */
|
|
259
|
+
sessionNotices(sessionId) {
|
|
260
|
+
const redact = this.deps.redactEnabled();
|
|
261
|
+
return this.notices.list(sessionId).map((n) => ({ ...n, text: redactString(n.text, redact) }));
|
|
262
|
+
}
|
|
263
|
+
/** TAL-808: the session's standing goal (session `goal`). */
|
|
264
|
+
sessionGoal(sessionId) {
|
|
265
|
+
const goal = this.goals.view(sessionId);
|
|
266
|
+
if (!goal)
|
|
267
|
+
return null;
|
|
268
|
+
const redact = this.deps.redactEnabled();
|
|
269
|
+
return { ...goal, text: redactString(goal.text, redact), reason: goal.reason === null ? null : redactString(goal.reason, redact) };
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* TAL-808: reads the session's goal from the Agent and, when its view changed, sends `session_goal` on `put` (else the
|
|
273
|
+
* session's live stream) and `sessions_changed`, which reloads the session for a client between turns.
|
|
274
|
+
*/
|
|
275
|
+
async refreshGoal(s, put = async (event, data) => { (await this.emitToSession(s.session_id, event, data)); }) {
|
|
276
|
+
if (!(await this.readGoal(s)))
|
|
277
|
+
return;
|
|
278
|
+
await put('session_goal', { session_id: s.session_id, goal: this.sessionGoal(s.session_id) });
|
|
279
|
+
this.deps.events.publish('goal', { profile: s.profile, sessionId: s.session_id });
|
|
280
|
+
}
|
|
281
|
+
/** TAL-808: a session's first detail after a restart reads its goal before answering, so it tells no one else. */
|
|
282
|
+
async loadGoal(sessionId) {
|
|
283
|
+
if (this.goals.has(sessionId))
|
|
284
|
+
return;
|
|
285
|
+
let s;
|
|
286
|
+
try {
|
|
287
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
await this.readGoal(s);
|
|
293
|
+
}
|
|
294
|
+
/** 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. */
|
|
295
|
+
async readGoal(s) {
|
|
296
|
+
const sidecar = this.deps.sidecar();
|
|
297
|
+
if (!sidecar)
|
|
298
|
+
return false;
|
|
299
|
+
let status;
|
|
300
|
+
try {
|
|
301
|
+
// Short: a session's first load waits on it.
|
|
302
|
+
status = await sidecar.call('goals.get', { session_id: s.session_id, profile_home: this.deps.profileHome(s.profile) }, { timeoutMs: 3_000 });
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
this.deps.log(`[webui] WARNING: goal read failed for ${s.session_id}: ${error.message}`);
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
const changed = this.goals.set(s.session_id, status);
|
|
309
|
+
this.goals.rereadAfterPark(s.session_id, () => { void this.refreshGoal({ session_id: s.session_id, profile: s.profile }); });
|
|
310
|
+
return changed;
|
|
311
|
+
}
|
|
312
|
+
/** Hygiene sweep: a deleted session's hold, notices and goal go with it (TAL-899, TAL-803, TAL-808). */
|
|
313
|
+
async sweep() {
|
|
314
|
+
const exists = async (sid) => (await this.deps.store.exists(sid));
|
|
315
|
+
for (const sid of this.stopHeld)
|
|
316
|
+
if (!(await exists(sid)))
|
|
317
|
+
this.stopHeld.delete(sid);
|
|
318
|
+
await this.notices.sweep(exists);
|
|
319
|
+
await this.goals.sweep(exists);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Admit automatic turns through the same workspace recovery and model resolution as user turns. After the user's Stop
|
|
323
|
+
* none starts (TAL-899): the answer is a `held` 409, and the caller keeps its work for the user's next turn.
|
|
324
|
+
*/
|
|
325
|
+
async startServerTurn(session, opts) {
|
|
326
|
+
if ((await this.automaticTurnsHeld(session.session_id)))
|
|
327
|
+
return { _status: 409, error: 'automatic turns are held after Stop', held: true };
|
|
328
|
+
try {
|
|
329
|
+
const service = this.deps.service();
|
|
330
|
+
const workspace = (await service.turnWorkspace(session));
|
|
331
|
+
const [model, modelProvider, normalizedModel] = service.turnModel(session);
|
|
332
|
+
return (await this.admit(session, { ...opts, workspace, model, modelProvider, normalizedModel }));
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (error instanceof HttpFailure)
|
|
336
|
+
return { _status: error.status, error: error.message };
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** A user's turn; admitting it releases the hold a Stop put on automatic turns (TAL-899). */
|
|
341
|
+
async start(session, opts) {
|
|
342
|
+
const response = (await this.admit(session, opts, true));
|
|
343
|
+
if (response._status === undefined)
|
|
344
|
+
this.stopHeld.delete(session.session_id);
|
|
345
|
+
return response;
|
|
346
|
+
}
|
|
230
347
|
/** Python `_start_chat_stream_for_session`: persist pending state, register the channel, launch the worker. */
|
|
231
|
-
|
|
348
|
+
async admit(session, opts, releaseStopHold = false) {
|
|
349
|
+
const timing = opts.timing ?? new StartupTiming(this.deps.log);
|
|
350
|
+
timing.identify(session.session_id);
|
|
351
|
+
timing.begin('admission');
|
|
352
|
+
try {
|
|
353
|
+
const response = await this.admitTimed(session, { ...opts, timing }, releaseStopHold);
|
|
354
|
+
if (response._status !== undefined)
|
|
355
|
+
timing.finish('refused');
|
|
356
|
+
else if (!response.stream_id || !this.workers.has(response.stream_id))
|
|
357
|
+
timing.finish('noop');
|
|
358
|
+
return response;
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
timing.finish(timingOutcome(error));
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async admitTimed(session, opts, releaseStopHold) {
|
|
366
|
+
const timing = opts.timing;
|
|
367
|
+
const closeChecks = timing.begin('admission_checks');
|
|
232
368
|
const s = session;
|
|
369
|
+
const readOnly = (await this.deps.service().readOnlyReason(s));
|
|
370
|
+
if (readOnly)
|
|
371
|
+
return { error: readOnly, _status: 403 };
|
|
233
372
|
if (this.deps.updateInProgress?.())
|
|
234
373
|
return { error: 'Web is updating. Retry after it restarts.', _status: 503 };
|
|
235
374
|
if (this.deps.profileDeleting?.(s.profile ?? null))
|
|
@@ -242,7 +381,7 @@ export class TurnRunner {
|
|
|
242
381
|
if (locked) {
|
|
243
382
|
if (this.registry.liveIds.has(locked) || this.registry.activeRuns.has(locked))
|
|
244
383
|
return { error: 'session already has an active stream', active_stream_id: locked, _status: 409 };
|
|
245
|
-
this.deps.service().clearStaleStreamState(s);
|
|
384
|
+
await this.deps.service().clearStaleStreamState(s);
|
|
246
385
|
if (s.active_stream_id)
|
|
247
386
|
return { error: 'session already has an active stream', active_stream_id: s.active_stream_id, _status: 409 };
|
|
248
387
|
}
|
|
@@ -251,11 +390,28 @@ export class TurnRunner {
|
|
|
251
390
|
if (blocking)
|
|
252
391
|
return { error: 'session already has an active stream', active_stream_id: blocking, _status: 409 };
|
|
253
392
|
}
|
|
393
|
+
const accepted = isDict(s.extra.completion_acceptances) ? s.extra.completion_acceptances : {};
|
|
394
|
+
if (opts.completionKeys?.length && opts.completionKeys.every((key) => typeof accepted[key] === 'string' && accepted[key])) {
|
|
395
|
+
timing.identify(s.session_id, str(accepted[opts.completionKeys[0]]));
|
|
396
|
+
timing.finish('noop');
|
|
397
|
+
return { stream_id: str(accepted[opts.completionKeys[0]]), session_id: s.session_id };
|
|
398
|
+
}
|
|
399
|
+
closeChecks();
|
|
254
400
|
const streamId = randomUUID().replace(/-/g, '');
|
|
401
|
+
timing.identify(s.session_id, streamId);
|
|
255
402
|
const wasHiddenEmpty = !s.messages.length && !s.context_messages.length && !s.hasPendingPrompt;
|
|
256
403
|
const attachments = opts.attachments ?? [];
|
|
257
404
|
// A background wakeup stays one in a forked session too (TAL-460).
|
|
258
405
|
const source = opts.source !== 'process_wakeup' && str(s.source_tag).toLowerCase() === 'fork' ? 'fork' : (opts.source ?? 'webui');
|
|
406
|
+
// TAL-1005: the context figures the last turn reported hold for a turn on the same model only, and are judged before
|
|
407
|
+
// admission clears the post-compression estimate. A `/btw` clone is thrown away after one answer, so it is never due.
|
|
408
|
+
const sameRuntime = str(s.model) === str(opts.model) && str(s.model_provider) === str(opts.modelProvider);
|
|
409
|
+
const compactionDue = !opts.ephemeral && sameRuntime && this.deps.service().compactionDue(s);
|
|
410
|
+
if (!sameRuntime)
|
|
411
|
+
forgetContextFigures(s);
|
|
412
|
+
const notificationOwner = this.deps.notificationOwner();
|
|
413
|
+
if (notificationOwner)
|
|
414
|
+
s.extra.notification_owner = notificationOwner;
|
|
259
415
|
s.workspace = opts.workspace;
|
|
260
416
|
s.model = opts.model;
|
|
261
417
|
s.model_provider = opts.modelProvider;
|
|
@@ -265,6 +421,12 @@ export class TurnRunner {
|
|
|
265
421
|
s.pending_attachments = attachments;
|
|
266
422
|
s.pending_started_at = this.deps.now();
|
|
267
423
|
s.pending_user_source = source;
|
|
424
|
+
// TAL-927: reasoning one provider issued can fail on another (hermes-agent#122831), so a provider switch moves the
|
|
425
|
+
// boundary its carriers replay from.
|
|
426
|
+
if (s.reasoning_replay_provider !== str(opts.modelProvider)) {
|
|
427
|
+
s.reasoning_replay_provider = str(opts.modelProvider);
|
|
428
|
+
s.reasoning_replay_from = s.pending_started_at;
|
|
429
|
+
}
|
|
268
430
|
if (['', 'Untitled', 'New Chat'].includes(str(s.title).trim())) {
|
|
269
431
|
const provisional = titleFrom([{ role: 'user', content: opts.msg }], s.title || 'Untitled');
|
|
270
432
|
if (provisional && !['', 'Untitled', 'New Chat'].includes(provisional.trim()))
|
|
@@ -272,21 +434,47 @@ export class TurnRunner {
|
|
|
272
434
|
}
|
|
273
435
|
if (this.deps.saveMode() === 'eager')
|
|
274
436
|
this.checkpointUserMessage(s, opts.msg, attachments, s.pending_started_at, source, streamId);
|
|
275
|
-
|
|
437
|
+
const previousStopHold = s.automatic_turns_stop_held;
|
|
438
|
+
if (releaseStopHold)
|
|
439
|
+
s.automatic_turns_stop_held = false;
|
|
440
|
+
const previousAcceptances = s.extra.completion_acceptances;
|
|
441
|
+
if (opts.completionKeys?.length)
|
|
442
|
+
s.extra.completion_acceptances = { ...accepted, ...Object.fromEntries(opts.completionKeys.map((key) => [key, streamId])) };
|
|
443
|
+
const previousUpdates = s.extra.background_updates;
|
|
444
|
+
if (opts.backgroundUpdate)
|
|
445
|
+
recordBackgroundUpdate(s, streamId, opts.backgroundUpdate);
|
|
446
|
+
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 };
|
|
447
|
+
this.registry.registerActiveRun(admission);
|
|
448
|
+
try {
|
|
449
|
+
await timing.measure('admission_persistence', () => this.deps.store.save(s));
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
this.registry.activeRuns.delete(streamId);
|
|
453
|
+
s.extra.background_updates = previousUpdates;
|
|
454
|
+
s.extra.completion_acceptances = previousAcceptances;
|
|
455
|
+
s.automatic_turns_stop_held = previousStopHold;
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
276
458
|
this.registry.writebackOwners.set(s.session_id, streamId);
|
|
277
459
|
// A new chat appears in the list; every turn tells open clients a run started (TAL-434).
|
|
278
460
|
this.deps.events.publish(wasHiddenEmpty ? 'session_new' : 'turn_started', { profile: s.profile, sessionId: s.session_id });
|
|
279
461
|
try {
|
|
280
|
-
this.deps.
|
|
462
|
+
this.deps.onTurnStart?.(s.session_id);
|
|
463
|
+
}
|
|
464
|
+
catch { /* best effort */ }
|
|
465
|
+
try {
|
|
466
|
+
await timing.measure('workspace_persistence', () => this.deps.workspaces.setLastWorkspace(opts.workspace, s.profile));
|
|
281
467
|
}
|
|
282
468
|
catch { /* best effort */ }
|
|
283
469
|
const channel = this.registry.create(streamId, s.session_id);
|
|
470
|
+
admission.admitting = false;
|
|
284
471
|
if (opts.goalRelated)
|
|
285
472
|
this.registry.goalRelated.add(streamId);
|
|
286
|
-
|
|
287
|
-
const worker = this.run(s.session_id, streamId, channel, opts).catch((error) => {
|
|
473
|
+
timing.end('admission');
|
|
474
|
+
const worker = this.run(s.session_id, streamId, channel, opts, compactionDue).catch((error) => {
|
|
475
|
+
timing.finish(timingOutcome(error));
|
|
288
476
|
this.deps.log(`[webui] ERROR turn ${streamId} crashed\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
289
|
-
}).finally(() => { this.workers.delete(streamId); });
|
|
477
|
+
}).finally(() => { timing.finish(this.registry.cancelled.has(streamId) ? 'cancelled' : undefined); this.workers.delete(streamId); });
|
|
290
478
|
this.workers.set(streamId, worker);
|
|
291
479
|
const response = { stream_id: streamId, session_id: s.session_id, pending_started_at: s.pending_started_at, turn_id: streamId, title: s.title };
|
|
292
480
|
if (opts.normalizedModel && opts.model)
|
|
@@ -307,28 +495,78 @@ export class TurnRunner {
|
|
|
307
495
|
s.messages.push(pendingUserRow(msg, attachments, startedAt, source, turnId));
|
|
308
496
|
}
|
|
309
497
|
// ── worker ───────────────────────────────────────────────────────────────
|
|
310
|
-
async run(sessionId, streamId, channel, opts) {
|
|
498
|
+
async run(sessionId, streamId, channel, opts, compactionDue) {
|
|
311
499
|
const { deps } = this;
|
|
500
|
+
const timing = opts.timing;
|
|
501
|
+
const closePreparation = timing?.begin('worker_preparation');
|
|
312
502
|
let writer = null;
|
|
313
503
|
try {
|
|
314
|
-
writer = deps.journal.writer(sessionId, streamId);
|
|
504
|
+
writer = (await deps.journal.writer(sessionId, streamId));
|
|
315
505
|
this.writers.set(streamId, writer);
|
|
316
506
|
}
|
|
317
507
|
catch (error) {
|
|
318
508
|
deps.log(`[webui] WARNING: run journal degraded for stream ${streamId}: ${error.message}`);
|
|
319
509
|
}
|
|
320
510
|
const failure = { apperror: null, cancelled: false };
|
|
321
|
-
|
|
511
|
+
let waitDetail = null;
|
|
512
|
+
let providerPhase = null;
|
|
513
|
+
let statusEnded = false;
|
|
514
|
+
// TAL-1006: the open compression span closes on the Agent's end of it, else on the next content frame.
|
|
515
|
+
const closeCompression = async (createdAt) => {
|
|
516
|
+
if (!this.compressionSpans.has(streamId))
|
|
517
|
+
return;
|
|
518
|
+
await put('compressed', { session_id: sessionId, message: 'Compression finished' }, createdAt === undefined ? {} : { createdAt });
|
|
519
|
+
};
|
|
520
|
+
const put = async (event, data, meta = {}) => {
|
|
521
|
+
if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn', 'run_status'].includes(event))
|
|
522
|
+
return;
|
|
523
|
+
const terminal = ['done', 'cancel', 'apperror', 'error', 'stream_end'].includes(event);
|
|
524
|
+
if (terminal) {
|
|
525
|
+
statusEnded = true;
|
|
526
|
+
for (const call of this.registry.liveToolCalls.get(streamId) ?? [])
|
|
527
|
+
delete call.progress;
|
|
528
|
+
}
|
|
529
|
+
if (CONTENT_EVENTS.has(event) || terminal)
|
|
530
|
+
providerPhase = null;
|
|
531
|
+
if (event === 'rate_limit_wait')
|
|
532
|
+
waitDetail = data.wait;
|
|
533
|
+
// Both pre-turn compaction and Agent callbacks publish through this boundary.
|
|
534
|
+
if (event === 'compressing' && !statusEnded) {
|
|
535
|
+
const startedAt = this.compressionSpans.get(streamId) ?? meta.createdAt ?? deps.now();
|
|
536
|
+
this.compressionSpans.set(streamId, startedAt);
|
|
537
|
+
data = { ...data, started_at: startedAt };
|
|
538
|
+
}
|
|
539
|
+
else if (event === 'compressed')
|
|
540
|
+
this.compressionSpans.delete(streamId);
|
|
541
|
+
if (CONTENT_EVENTS.has(event)) {
|
|
542
|
+
if (waitDetail) {
|
|
543
|
+
await put('rate_limit_wait', { session_id: sessionId, wait: null }, meta.createdAt === undefined ? {} : { createdAt: meta.createdAt });
|
|
544
|
+
}
|
|
545
|
+
await closeCompression(meta.createdAt);
|
|
546
|
+
}
|
|
547
|
+
const status = event === 'run_status' ? data.status : statusEnded ? null : this.compressionSpans.has(streamId) ? { phase: 'compressing' }
|
|
548
|
+
: waitDetail ? { phase: 'rate_limit_wait', wait: waitDetail } : providerPhase ?? { phase: 'working' };
|
|
549
|
+
if (JSON.stringify(this.runStatuses.get(streamId) ?? null) !== JSON.stringify(status)) {
|
|
550
|
+
if (status)
|
|
551
|
+
this.runStatuses.set(streamId, status);
|
|
552
|
+
else
|
|
553
|
+
this.runStatuses.delete(streamId);
|
|
554
|
+
await put('run_status', { session_id: sessionId, status }, meta);
|
|
555
|
+
}
|
|
556
|
+
// The sidecar phase is private; only the selected status crosses the client boundary.
|
|
557
|
+
if (event === 'provider_phase')
|
|
558
|
+
return;
|
|
322
559
|
if (event === 'apperror')
|
|
323
560
|
failure.apperror = data;
|
|
324
561
|
else if (event === 'cancel')
|
|
325
562
|
failure.cancelled = true;
|
|
326
563
|
for (const steerEvent of this.takeSteerEventsBefore(streamId, event))
|
|
327
|
-
put(steerEvent[0], steerEvent[1]);
|
|
328
|
-
if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn'].includes(event))
|
|
564
|
+
(await put(steerEvent[0], steerEvent[1]));
|
|
565
|
+
if (this.registry.cancelled.has(streamId) && !['cancel', 'apperror', 'steer_consumed', 'steer_withdrawn', 'run_status'].includes(event))
|
|
329
566
|
return;
|
|
330
567
|
// `cancel()` already wrote the terminal row and closed the stream: the worker's unwind adds no second one.
|
|
331
568
|
if (event === 'cancel' && !this.registry.streams.has(streamId)) {
|
|
569
|
+
timing?.frame(event, data);
|
|
332
570
|
try {
|
|
333
571
|
deps.onTerminal?.(streamId, 'cancelled');
|
|
334
572
|
}
|
|
@@ -336,9 +574,9 @@ export class TurnRunner {
|
|
|
336
574
|
return;
|
|
337
575
|
}
|
|
338
576
|
let eventId = null;
|
|
339
|
-
if (writer) {
|
|
577
|
+
if (writer && event !== 'tool_progress') {
|
|
340
578
|
try {
|
|
341
|
-
eventId = writer.appendSseEvent(event, data, meta).event_id;
|
|
579
|
+
eventId = (await writer.appendSseEvent(event, data, meta)).event_id;
|
|
342
580
|
}
|
|
343
581
|
catch (error) {
|
|
344
582
|
this.registry.degradedJournals.add(streamId);
|
|
@@ -346,6 +584,7 @@ export class TurnRunner {
|
|
|
346
584
|
}
|
|
347
585
|
}
|
|
348
586
|
channel.put([event, data, eventId, meta.redacted]);
|
|
587
|
+
timing?.frame(event, data);
|
|
349
588
|
if (event === 'done' || event === 'cancel' || event === 'apperror' || event === 'error') {
|
|
350
589
|
// Relay's phase follows the turn outcome, so a background Live Activity ends as the app would end it.
|
|
351
590
|
const state = str(data.terminal_state);
|
|
@@ -360,25 +599,46 @@ export class TurnRunner {
|
|
|
360
599
|
const activeRun = this.registry.activeRuns.get(streamId);
|
|
361
600
|
let s;
|
|
362
601
|
try {
|
|
363
|
-
s = deps.store.get(sessionId);
|
|
602
|
+
s = (await deps.store.get(sessionId));
|
|
364
603
|
}
|
|
365
604
|
catch {
|
|
366
|
-
put('apperror', { type: 'error', terminal_state: 'error', message: 'Session not found', session_id: sessionId });
|
|
367
|
-
this.teardown(sessionId, streamId);
|
|
605
|
+
await put('apperror', { type: 'error', terminal_state: 'error', message: 'Session not found', session_id: sessionId });
|
|
606
|
+
await this.teardown(sessionId, streamId);
|
|
368
607
|
return;
|
|
369
608
|
}
|
|
609
|
+
const notificationScope = { owner: str(s.extra.notification_owner) || 'local-owner', profile: s.profile ?? 'default' };
|
|
370
610
|
const msgText = opts.msg;
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
611
|
+
const history = async () => {
|
|
612
|
+
// TAL-493: one state.db read builds the history and marks where this turn's own rows begin.
|
|
613
|
+
const reading = deps.service().beginStateDbTurn(s);
|
|
614
|
+
// TAL-1162: the read runs on the state.db worker. Until it lands, a Stop settles from the local history with no
|
|
615
|
+
// starting id, as a turn whose starting read failed does.
|
|
616
|
+
const local = this.turnHistory(s, streamId, []);
|
|
617
|
+
this.stopContexts.set(streamId, { previousContext: local.previousContext, historyLength: local.apiHistory.length, prompt: msgText, msgText, checkpointed: false, stateDbStartId: null, stateDbStartGeneration: null, startReadPending: true });
|
|
618
|
+
const startRead = await reading;
|
|
619
|
+
const previousMessages = structuredClone(s.messages);
|
|
620
|
+
const { previousContext, apiHistory } = this.turnHistory(s, streamId, startRead.rows);
|
|
621
|
+
// A Stop that landed during the read already took the local entry.
|
|
622
|
+
if (!this.registry.cancelled.has(streamId))
|
|
623
|
+
this.stopContexts.set(streamId, { previousContext, historyLength: apiHistory.length, prompt: msgText, msgText, checkpointed: false, stateDbStartId: startRead.ok ? stateDbSeenId(startRead.rows) ?? 0 : null, stateDbStartGeneration: startRead.generation, startReadPending: false });
|
|
624
|
+
return { previousMessages, previousContext, apiHistory };
|
|
625
|
+
};
|
|
380
626
|
// Before the first await: a Stop can land at any point after admission.
|
|
381
|
-
|
|
627
|
+
const controller = new AbortController();
|
|
628
|
+
this.abortControllers.set(streamId, controller);
|
|
629
|
+
let { previousMessages, previousContext, apiHistory } = await history();
|
|
630
|
+
// TAL-1005: an over-threshold session compacts in its joinable compression job before the turn reads its history, so
|
|
631
|
+
// a Stop ends only this wait and a resend joins the same job. The turn then reads the compacted session afresh.
|
|
632
|
+
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' })); } });
|
|
633
|
+
if (compacted) {
|
|
634
|
+
try {
|
|
635
|
+
s = (await deps.store.get(sessionId));
|
|
636
|
+
}
|
|
637
|
+
catch { /* the cancel or error path below reports a deleted session */ }
|
|
638
|
+
;
|
|
639
|
+
({ previousMessages, previousContext, apiHistory } = await history());
|
|
640
|
+
await put('compressed', this.compressedFrame(s));
|
|
641
|
+
}
|
|
382
642
|
const activeTurnToken = buildActiveTurnToken(streamId, s.pending_started_at);
|
|
383
643
|
const sidecar = deps.sidecar();
|
|
384
644
|
const partialText = this.registry.partialText.get(streamId) ?? [];
|
|
@@ -395,21 +655,19 @@ export class TurnRunner {
|
|
|
395
655
|
// TAL-397: the live meter. Counters start from the session's and follow the Agent's; the rate counts text deltas.
|
|
396
656
|
const liveUsage = { input_tokens: s.input_tokens, output_tokens: s.output_tokens, estimated_cost: s.estimated_cost, cache_read_tokens: s.cache_read_tokens, cache_write_tokens: s.cache_write_tokens };
|
|
397
657
|
const meter = { deltas: 0, first: 0, last: 0, sentAt: null };
|
|
398
|
-
const putMetering = (force) => {
|
|
399
|
-
const now = deps.now();
|
|
658
|
+
const putMetering = async (force, now = deps.now()) => {
|
|
400
659
|
if (!force && meter.sentAt !== null && now - meter.sentAt < METERING_INTERVAL_S)
|
|
401
660
|
return;
|
|
402
661
|
meter.sentAt = now;
|
|
403
662
|
const tps = meter.deltas && meter.last > meter.first ? Math.round((meter.deltas / (meter.last - meter.first)) * 10) / 10 : null;
|
|
404
|
-
put('metering', { session_id: sessionId, usage: meteringUsage(liveUsage), tps, tps_available: tps !== null, estimated: false });
|
|
663
|
+
await put('metering', { session_id: sessionId, usage: meteringUsage(liveUsage), tps, tps_available: tps !== null, estimated: false });
|
|
405
664
|
};
|
|
406
|
-
const meterDelta = () => {
|
|
407
|
-
const now = deps.now();
|
|
665
|
+
const meterDelta = async (now = deps.now()) => {
|
|
408
666
|
if (!meter.deltas)
|
|
409
667
|
meter.first = now;
|
|
410
668
|
meter.last = now;
|
|
411
669
|
meter.deltas += 1;
|
|
412
|
-
putMetering(false);
|
|
670
|
+
await putMetering(false, now);
|
|
413
671
|
};
|
|
414
672
|
// TAL-460: a background turn's text is held back while it could still be a silence marker, so one never flashes by.
|
|
415
673
|
// ponytail: checks the turn's whole text, so a marker after earlier prose streams until the settled row hides it;
|
|
@@ -418,13 +676,11 @@ export class TurnRunner {
|
|
|
418
676
|
let heldText = '';
|
|
419
677
|
const titles = new ReasoningTitleTracker();
|
|
420
678
|
let capturedTerminalError = null;
|
|
421
|
-
const controller = new AbortController();
|
|
422
|
-
this.abortControllers.set(streamId, controller);
|
|
423
679
|
const attachments = opts.attachments ?? [];
|
|
424
680
|
// TAL-545: one image mode decides both this turn's upload and the replayed history, which a text-mode turn sends
|
|
425
681
|
// without native images; only a turn with an image on either side asks for it.
|
|
426
682
|
const imageMode = attachments.some(isImageAttachment) || hasNativeImages(apiHistory) ? await this.imageInputMode(sessionId, s, opts, controller.signal) : null;
|
|
427
|
-
const userMessage = this.buildUserMessage(msgText, attachments, opts.workspace, sessionId, imageMode);
|
|
683
|
+
const userMessage = this.buildUserMessage(opts.prompt ?? msgText, attachments, opts.workspace, sessionId, imageMode);
|
|
428
684
|
const conversationHistory = imageMode === 'text' ? withoutNativeImages(apiHistory) : apiHistory;
|
|
429
685
|
// The prompt the Agent actually gets, native image parts included.
|
|
430
686
|
const stop = this.stopContexts.get(streamId);
|
|
@@ -434,6 +690,10 @@ export class TurnRunner {
|
|
|
434
690
|
activeRun.phase = 'running';
|
|
435
691
|
const settledAt = { value: false };
|
|
436
692
|
let succeeded = false;
|
|
693
|
+
let ownershipRefused = false;
|
|
694
|
+
let agentDispatched = false;
|
|
695
|
+
let streamWrites = Promise.resolve();
|
|
696
|
+
let streamWriteError;
|
|
437
697
|
// TAL-424: steers an errored turn ended without taking, sent as one follow-up turn once this one is torn down.
|
|
438
698
|
let followUp = [];
|
|
439
699
|
try {
|
|
@@ -443,6 +703,11 @@ export class TurnRunner {
|
|
|
443
703
|
// system message carries the frozen session workspace, the WebUI guidance rides as the ephemeral prompt with this
|
|
444
704
|
// turn's workspace (TAL-710: never a tag on the user's message, which the Agent can persist from memory).
|
|
445
705
|
const cfg = (await deps.profileConfig?.(s.profile ?? null)) ?? {};
|
|
706
|
+
const personality = str(s.personality);
|
|
707
|
+
// A selected personality never silently drops: an unknown catalog fails the turn (an absent name is the Agent's no-overlay).
|
|
708
|
+
const personalities = personality && deps.personalities
|
|
709
|
+
? 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' }); })
|
|
710
|
+
: [];
|
|
446
711
|
// The sidecar's per-session YOLO state does not survive its restarts: push the local flag (either way) before
|
|
447
712
|
// the turn so a stale sidecar-side enable cannot auto-approve a session the UI reports as guarded.
|
|
448
713
|
await this.withYoloLock(sessionId, async () => { try {
|
|
@@ -452,173 +717,288 @@ export class TurnRunner {
|
|
|
452
717
|
// Last yield before `chat.start`: a cancel that landed during the attachment, config or YOLO awaits must not
|
|
453
718
|
// start an Agent turn (the client also refuses an already-aborted signal outright).
|
|
454
719
|
if (this.registry.cancelled.has(streamId)) {
|
|
455
|
-
this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
456
|
-
put('cancel', this.cancelFrame(sessionId));
|
|
720
|
+
await this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
721
|
+
await put('cancel', (await this.cancelFrame(sessionId)));
|
|
457
722
|
return;
|
|
458
723
|
}
|
|
459
724
|
const turnContext = {
|
|
460
725
|
system_message: workspaceSystemMessage(str(s.created_workspace) || str(s.workspace)),
|
|
461
|
-
ephemeral_system_prompt: webuiEphemeralSystemPrompt({ config: cfg,
|
|
462
|
-
|
|
463
|
-
max_tokens: maxTokensFromConfig(cfg),
|
|
464
|
-
reasoning_config: reasoningConfigFromConfig(cfg, opts.modelProvider ?? s.model_provider ?? null),
|
|
726
|
+
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' }),
|
|
727
|
+
...agentBuildParams(cfg, opts.source ?? 'webui', opts.modelProvider ?? s.model_provider ?? null, deps.env ?? {}),
|
|
465
728
|
};
|
|
466
729
|
// Python `_public_prefill_context_status`: the session-recall prefill hook was dropped with TAL-245, so the
|
|
467
730
|
// context frame always reports the not-configured shape.
|
|
468
|
-
const
|
|
469
|
-
|
|
731
|
+
const persistentHome = deps.profileHome(s.profile);
|
|
732
|
+
const persistentBefore = await persistentStateSnapshot(persistentHome);
|
|
733
|
+
await put('context_status', { session_id: sessionId, prefill: { status: 'not_configured', source: 'none', label: '', message_count: 0 } });
|
|
734
|
+
// Setup awaits can outlive a gateway adoption, so the owner is rechecked right before dispatch.
|
|
735
|
+
await deps.service().assertWritable(s);
|
|
736
|
+
closePreparation?.();
|
|
737
|
+
timing?.emit('dispatch');
|
|
738
|
+
agentDispatched = true;
|
|
470
739
|
const result = await sidecar.call('chat.start', {
|
|
471
|
-
profile_home: deps.profileHome(s.profile), session_id: sessionId, stream_id: streamId,
|
|
740
|
+
profile_home: deps.profileHome(s.profile), session_id: sessionId, stream_id: streamId, ...this.agentWorkspace(s, opts.workspace), model: opts.model ?? '', model_provider: opts.modelProvider,
|
|
472
741
|
user_message: userMessage, ...turnContext, conversation_history: conversationHistory, enabled_toolsets: deps.toolsetsFor(s), ...(msgText ? { persist_user_message: msgText } : {}), ...(opts.ephemeral ? { ephemeral: true } : {}),
|
|
742
|
+
...(opts.voiceTurn ? { voice_turn: true } : {}),
|
|
743
|
+
...(opts.connectCard ? { connect_card: true } : {}),
|
|
744
|
+
...(opts.skillSecret ? { skill_secret: true } : {}),
|
|
473
745
|
}, {
|
|
746
|
+
timing,
|
|
474
747
|
signal: controller.signal,
|
|
475
748
|
timeoutMs: 0,
|
|
476
749
|
onStream: (frame) => {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
partialText.push(str(data.text));
|
|
487
|
-
if (holdSilence && mayBecomeSilentReply(partialText.join(''))) {
|
|
488
|
-
heldText += str(data.text);
|
|
489
|
-
meterDelta();
|
|
490
|
-
return;
|
|
750
|
+
timing?.observe('first_sidecar_output');
|
|
751
|
+
const receivedAt = deps.now();
|
|
752
|
+
streamWrites = streamWrites.then(async () => {
|
|
753
|
+
const publish = put;
|
|
754
|
+
const emitFrame = (event, data, meta = {}) => publish(event, data, { ...meta, createdAt: receivedAt });
|
|
755
|
+
const data = isDict(frame.data) ? frame.data : {};
|
|
756
|
+
if ((frame.event === 'notice' || frame.event === 'notice_clear') && str(data.key).startsWith('startup-warning.')) {
|
|
757
|
+
try {
|
|
758
|
+
await deps.notifications.agentWarning(notificationScope, str(data.key), frame.event === 'notice' ? { text: str(data.text), level: data.level } : null);
|
|
491
759
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
case 'reasoning': {
|
|
498
|
-
if (partialText.length && !reasoningText.length)
|
|
499
|
-
titles.resetSegment();
|
|
500
|
-
reasoningText.push(str(data.text));
|
|
501
|
-
// Python `reasoning_event_payload`: bold or first-line titles ride on the delta they complete.
|
|
502
|
-
put('reasoning', titles.prepare(reasoningEventPayload(str(data.text), reasoningText.join(''))));
|
|
503
|
-
meterDelta();
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
case 'usage': {
|
|
507
|
-
const usage = ChatUsageSchema.safeParse(data);
|
|
508
|
-
if (usage.success) {
|
|
509
|
-
applyAgentUsage(liveUsage, usage.data);
|
|
510
|
-
this.liveUsage.set(streamId, liveUsage);
|
|
760
|
+
catch (error) {
|
|
761
|
+
// Notification storage must not turn a successful Agent result into a failed chat.
|
|
762
|
+
deps.log(`[webui] startup warning persistence failed for ${sessionId}: ${String(error)}`);
|
|
511
763
|
}
|
|
512
|
-
putMetering(true);
|
|
513
764
|
return;
|
|
514
765
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
766
|
+
switch (frame.event) {
|
|
767
|
+
case 'token': {
|
|
768
|
+
tokenSent = true;
|
|
769
|
+
firstTokenAt ??= receivedAt;
|
|
770
|
+
// Visible text closes the reasoning segment: a stable title snapshot (or a clear) goes out first.
|
|
771
|
+
const stable = titles.stableSnapshot(reasoningText.join(''));
|
|
772
|
+
if (stable !== null)
|
|
773
|
+
(await emitFrame('reasoning', { text: '', titles: stable }));
|
|
774
|
+
partialText.push(str(data.text));
|
|
775
|
+
if (holdSilence && mayBecomeSilentReply(partialText.join(''))) {
|
|
776
|
+
heldText += str(data.text);
|
|
777
|
+
(await meterDelta(receivedAt));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
await emitFrame('token', { text: heldText + str(data.text) });
|
|
781
|
+
heldText = '';
|
|
782
|
+
await meterDelta(receivedAt);
|
|
519
783
|
return;
|
|
520
784
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
put('tool', publicToolFrame(withToolId(data, toolIds.get(call)), redacted), { redacted });
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
case 'tool_complete': {
|
|
533
|
-
// TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
|
|
534
|
-
// are its display sections, which keep the stderr and exit code the flat preview drops.
|
|
535
|
-
const { raw_result: rawResult, todo_result: todoResult, result_diff: resultDiff, ...complete } = data;
|
|
536
|
-
const outcome = toolOutcome(rawResult);
|
|
537
|
-
complete.is_error = outcome.is_error;
|
|
538
|
-
if (rawResult !== undefined)
|
|
539
|
-
complete.result_view = outcome.result_view;
|
|
540
|
-
// TAL-448: a file edit's change, counted over the whole diff the sidecar forwards; redacted with the frame.
|
|
541
|
-
const editDiff = toolEditDiff(data.name, { diff: resultDiff });
|
|
542
|
-
if (editDiff)
|
|
543
|
-
complete.edit_diff = editDiff;
|
|
544
|
-
const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
|
|
545
|
-
if (tc) {
|
|
546
|
-
const startedAt = toolStartedAt.get(tc);
|
|
547
|
-
if (startedAt !== undefined)
|
|
548
|
-
complete.duration = Math.round(Math.max(0, deps.now() - startedAt) * 1000) / 1000;
|
|
549
|
-
// TAL-315: the decided view too, so a failed or cancelled turn's snapshot keeps the sections shown live.
|
|
550
|
-
Object.assign(tc, { done: true, snippet: data.preview, is_error: complete.is_error, duration: complete.duration ?? null, ...(complete.result_view ? { result_view: complete.result_view } : {}), ...(editDiff ? { edit_diff: editDiff } : {}) });
|
|
785
|
+
case 'reasoning': {
|
|
786
|
+
if (partialText.length && !reasoningText.length)
|
|
787
|
+
titles.resetSegment();
|
|
788
|
+
reasoningText.push(str(data.text));
|
|
789
|
+
// Python `reasoning_event_payload`: bold or first-line titles ride on the delta they complete.
|
|
790
|
+
await emitFrame('reasoning', titles.prepare(reasoningEventPayload(str(data.text), reasoningText.join(''))));
|
|
791
|
+
await meterDelta(receivedAt);
|
|
792
|
+
return;
|
|
551
793
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
this.liveTodos.set(streamId, todos);
|
|
561
|
-
put('todo_state', redactNestedMessageContainers({ ...todos, session_id: sessionId, stream_id: streamId, source: 'live' }, redacted));
|
|
794
|
+
case 'usage': {
|
|
795
|
+
const usage = ChatUsageSchema.safeParse(data);
|
|
796
|
+
if (usage.success) {
|
|
797
|
+
applyAgentUsage(liveUsage, usage.data);
|
|
798
|
+
this.liveUsage.set(streamId, liveUsage);
|
|
799
|
+
}
|
|
800
|
+
await putMetering(true, receivedAt);
|
|
801
|
+
return;
|
|
562
802
|
}
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
// TAL-514: the Agent stopped waiting on an approval (answer, timeout, or interrupt); withdraw its copy.
|
|
573
|
-
case 'approval_resolved': {
|
|
574
|
-
if (!str(data.approval_id))
|
|
803
|
+
case 'steer_pending': {
|
|
804
|
+
// TAL-424: while a withdraw or Send now rewrites the Agent's queue its pending text is ambiguous; it settles after.
|
|
805
|
+
if (this.steerRewrites.has(streamId)) {
|
|
806
|
+
this.deferredPendingSteer.set(streamId, str(data.text));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
// Capture the causal position before the next frame; only persistence is asynchronous.
|
|
810
|
+
const saving = this.settleConsumedSteers(sessionId, streamId, str(data.text));
|
|
811
|
+
await saving;
|
|
575
812
|
return;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
813
|
+
}
|
|
814
|
+
case 'tool': {
|
|
815
|
+
const call = { name: data.name, args: data.args ?? {}, tid: str(data.tid), done: false };
|
|
816
|
+
liveToolCalls.push(call);
|
|
817
|
+
toolIds.set(call, call.tid || mintToolId());
|
|
818
|
+
toolStartedAt.set(call, receivedAt);
|
|
819
|
+
const redacted = deps.redactEnabled();
|
|
820
|
+
await emitFrame('tool', publicToolFrame(withToolId(data, toolIds.get(call)), redacted), { redacted });
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
case 'tool_progress': {
|
|
824
|
+
if (statusEnded || this.registry.cancelled.has(streamId))
|
|
825
|
+
return;
|
|
826
|
+
const tc = liveToolCalls.findLast((call) => !call.done && call.tid === data.tool_call_id && Boolean(data.tool_call_id));
|
|
827
|
+
if (!tc)
|
|
828
|
+
return;
|
|
829
|
+
const redacted = deps.redactEnabled();
|
|
830
|
+
tc.progress = toolProgress({ ...data, message: typeof data.message === 'string' ? redactString(data.message, redacted) : null });
|
|
831
|
+
await emitFrame('tool_progress', { id: toolIds.get(tc), progress: tc.progress }, { redacted });
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
case 'tool_complete': {
|
|
835
|
+
// TAL-313: the server decides failure from the sidecar's raw result, which never leaves the server; TAL-315: so
|
|
836
|
+
// are its display sections, which keep the stderr and exit code the flat preview drops.
|
|
837
|
+
const { raw_result: rawResult, todo_result: todoResult, result_diff: resultDiff, display_metadata: displayMetadata, ...complete } = data;
|
|
838
|
+
const outcome = toolOutcome(rawResult, data.name);
|
|
839
|
+
complete.progress = null;
|
|
840
|
+
complete.is_error = outcome.is_error;
|
|
841
|
+
if (rawResult !== undefined)
|
|
842
|
+
complete.result_view = outcome.result_view;
|
|
843
|
+
// TAL-448: a file edit's change, counted over the whole diff the sidecar forwards; redacted with the frame. TAL-909:
|
|
844
|
+
// else the diff in the metadata the Agent persisted on the result row, which a reload reads the same way.
|
|
845
|
+
const editDiff = toolEditDiff(data.name, { diff: resultDiff }, displayMetadata);
|
|
846
|
+
if (editDiff)
|
|
847
|
+
complete.edit_diff = editDiff;
|
|
848
|
+
const tc = liveToolCalls[completedToolIndex(liveToolCalls, str(data.tid), data.name)];
|
|
849
|
+
if (tc) {
|
|
850
|
+
const startedAt = toolStartedAt.get(tc);
|
|
851
|
+
if (startedAt !== undefined)
|
|
852
|
+
complete.duration = Math.round(Math.max(0, receivedAt - startedAt) * 1000) / 1000;
|
|
853
|
+
// TAL-315: the decided view too, so a failed or cancelled turn's snapshot keeps the sections shown live.
|
|
854
|
+
Object.assign(tc, { done: true, progress: null, snippet: data.preview, is_error: complete.is_error, duration: complete.duration ?? null, ...(complete.result_view ? { result_view: complete.result_view } : {}), ...(editDiff ? { edit_diff: editDiff } : {}) });
|
|
855
|
+
}
|
|
856
|
+
if (startsBackgroundWork(data.name, rawResult))
|
|
857
|
+
deps.onBackgroundWork?.(sessionId, s.profile ?? null);
|
|
858
|
+
const id = (tc && toolIds.get(tc)) || str(data.tid) || mintToolId();
|
|
859
|
+
this.lastCompletedTool.set(streamId, id);
|
|
860
|
+
const redacted = deps.redactEnabled();
|
|
861
|
+
await emitFrame('tool_complete', publicToolFrame(withToolId(complete, id), redacted), { redacted });
|
|
862
|
+
// TAL-397: the todo tool's result is the session's new list; the settled session derives the same one.
|
|
863
|
+
// TAL-889: the sidecar attaches `todo_result` only to the Agent's todo tool, so no name check here.
|
|
864
|
+
const todos = parseTodoToolResult(todoResult);
|
|
865
|
+
if (todos) {
|
|
866
|
+
todos.ts = receivedAt;
|
|
867
|
+
this.liveTodos.set(streamId, todos);
|
|
868
|
+
await emitFrame('todo_state', redactNestedMessageContainers({ ...todos, session_id: sessionId, stream_id: streamId, source: 'live' }, redacted));
|
|
869
|
+
}
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
// Python: the live chat frame carries the queue head plus depth, not the entry that just arrived.
|
|
873
|
+
case 'approval': {
|
|
874
|
+
deps.pending.submitApproval(sessionId, { ...data, session_id: sessionId });
|
|
875
|
+
const { pending, pending_count } = deps.pending.approvalPending(sessionId);
|
|
876
|
+
await emitFrame('approval', { ...(pending ?? data), pending_count });
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
// TAL-514: the Agent stopped waiting on an approval (answer, timeout, or interrupt); withdraw its copy.
|
|
880
|
+
case 'approval_resolved': {
|
|
881
|
+
if (!str(data.approval_id))
|
|
882
|
+
return;
|
|
883
|
+
deps.pending.resolveApproval(sessionId, str(data.approval_id));
|
|
884
|
+
await emitFrame(...this.approvalHeadFrame(sessionId));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
case 'clarify': {
|
|
888
|
+
deps.pending.submitClarify(sessionId, { ...data, session_id: sessionId });
|
|
889
|
+
const head = deps.pending.clarifyHeadFrame(sessionId);
|
|
890
|
+
if (head)
|
|
891
|
+
(await emitFrame('clarify', head));
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
case 'clarify_resolved': {
|
|
895
|
+
const { head } = deps.pending.resolveClarify(sessionId, str(data.clarify_id));
|
|
896
|
+
if (head)
|
|
897
|
+
(await emitFrame('clarify', head));
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
case 'provider_phase': {
|
|
901
|
+
const phase = data.phase;
|
|
902
|
+
if (phase !== null && phase?.phase !== 'prompt_reading')
|
|
903
|
+
return;
|
|
904
|
+
const percent = phase?.progress_percent;
|
|
905
|
+
providerPhase = phase === null ? null : { phase: 'prompt_reading', progress_percent: typeof percent === 'number' && Number.isFinite(percent) ? Math.max(0, Math.min(100, percent)) : null };
|
|
906
|
+
await put('provider_phase', {}, { createdAt: receivedAt });
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
// TAL-821: the Agent sits out a provider's rate limit (`until`, epoch seconds: its reset, else the Agent's retry).
|
|
910
|
+
case 'rate_limit_wait': {
|
|
911
|
+
const until = typeof data.until === 'number' ? data.until : null;
|
|
912
|
+
if (until === null && !waitDetail)
|
|
913
|
+
return;
|
|
914
|
+
const provider = str(data.provider) || null;
|
|
915
|
+
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' } });
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
// TAL-818: a `manage_connections` card opened; clients poll and answer the operation by its `op_id`.
|
|
919
|
+
case 'connection': {
|
|
920
|
+
const snapshot = ConnectorOperationSnapshotSchema.safeParse(data);
|
|
921
|
+
if (snapshot.success)
|
|
922
|
+
(await emitFrame('connect_card', connectorOperation(snapshot.data, catalogNames(deps.profileHome(s.profile)))));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
case 'compressing':
|
|
926
|
+
await emitFrame('compressing', data);
|
|
927
|
+
return;
|
|
928
|
+
case 'compression_finished':
|
|
929
|
+
await closeCompression(receivedAt);
|
|
930
|
+
return;
|
|
931
|
+
case 'status':
|
|
932
|
+
if (data.kind === 'terminal_error' && !capturedTerminalError)
|
|
933
|
+
capturedTerminalError = str(data.message);
|
|
934
|
+
return;
|
|
935
|
+
// TAL-803: the session's notice list changed; every client replaces its copy.
|
|
936
|
+
case 'notice': {
|
|
937
|
+
const notice = this.notices.add(sessionId, data);
|
|
938
|
+
if (!notice)
|
|
939
|
+
return;
|
|
940
|
+
await emitFrame('notices', { session_id: sessionId, notices: this.sessionNotices(sessionId) });
|
|
941
|
+
// ponytail: the expiry reaches a client only while a turn streams; an idle tab drops it on its next session load.
|
|
942
|
+
if (notice.expires_at !== null) {
|
|
943
|
+
const expiresAt = notice.expires_at;
|
|
944
|
+
const emitAfterExpiry = async () => {
|
|
945
|
+
// Timers can run before the wall clock expires the notice. Let SessionNotices decide, then retry.
|
|
946
|
+
if (this.notices.list(sessionId).includes(notice))
|
|
947
|
+
setTimeout(() => { void emitAfterExpiry().catch((error) => { deps.log(`[webui] notice expiry failed: ${String(error)}`); }); }, Math.max(1, Math.ceil((expiresAt - deps.now()) * 1000))).unref();
|
|
948
|
+
else
|
|
949
|
+
(await this.emitToSession(sessionId, 'notices', { session_id: sessionId, notices: this.sessionNotices(sessionId) }));
|
|
950
|
+
};
|
|
951
|
+
await emitAfterExpiry();
|
|
952
|
+
}
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
case 'notice_clear':
|
|
956
|
+
case 'notices_reset':
|
|
957
|
+
if (frame.event === 'notice_clear' ? this.notices.clear(sessionId, str(data.key)) : this.notices.reset(sessionId))
|
|
958
|
+
(await emitFrame('notices', { session_id: sessionId, notices: this.sessionNotices(sessionId) }));
|
|
959
|
+
return;
|
|
960
|
+
default:
|
|
961
|
+
await emitFrame(frame.event, data);
|
|
592
962
|
}
|
|
593
|
-
|
|
594
|
-
if (data.kind === 'terminal_error' && !capturedTerminalError)
|
|
595
|
-
capturedTerminalError = str(data.message);
|
|
596
|
-
return;
|
|
597
|
-
default:
|
|
598
|
-
put(frame.event, data);
|
|
599
|
-
}
|
|
963
|
+
}).catch((error) => { streamWriteError = error instanceof Error ? error : new Error(String(error)); });
|
|
600
964
|
},
|
|
601
965
|
});
|
|
966
|
+
await streamWrites;
|
|
967
|
+
if (streamWriteError !== undefined)
|
|
968
|
+
throw streamWriteError;
|
|
602
969
|
settledAt.value = true;
|
|
603
970
|
// TAL-545: every transcript the Agent hands back settles with the history's images, not the request's projection.
|
|
604
971
|
const withImages = (rows) => withNativeImagesRestored(rows, conversationHistory, apiHistory);
|
|
605
972
|
result.messages = withImages(result.messages);
|
|
606
973
|
if (result.context_messages)
|
|
607
974
|
result.context_messages = withImages(result.context_messages);
|
|
975
|
+
// Snapshot IO must finish before the cancellation/ownership gates and final transcript writeback.
|
|
976
|
+
let persistentChanges = null;
|
|
977
|
+
if (!opts.ephemeral && result.status === 'completed' && !this.registry.cancelled.has(streamId)) {
|
|
978
|
+
try {
|
|
979
|
+
persistentChanges = persistentStateChanges(persistentBefore, await persistentStateSnapshot(persistentHome));
|
|
980
|
+
}
|
|
981
|
+
catch (error) {
|
|
982
|
+
deps.log(`[webui] persistent state change detection failed for ${sessionId}: ${error.message}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
// TAL-1162: the settlement's one state.db read runs on the worker before the gates below decide, so a Stop that
|
|
986
|
+
// lands during it takes the cancel path instead of racing this writeback.
|
|
987
|
+
const settlement = await deps.service().settlementRead(s);
|
|
608
988
|
if (this.registry.cancelled.has(streamId) || result.status === 'cancelled') {
|
|
609
989
|
// The Stop's pre-interrupt snapshot is the boundary, even when its reply lands after this result (the sidecar
|
|
610
990
|
// answers each request on its own thread); without one, the Agent's interrupted result is its canonical transcript.
|
|
611
991
|
const interrupted = await this.interrupts.get(streamId);
|
|
612
992
|
applyAgentUsage(liveUsage, result.usage);
|
|
613
993
|
this.liveUsage.set(streamId, liveUsage);
|
|
614
|
-
this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ? withImages(interrupted.checkpoint) : result.messages);
|
|
615
|
-
put('cancel', this.cancelFrame(sessionId));
|
|
994
|
+
await this.finalizeCancelled(s, streamId, opts.ephemeral, interrupted?.checkpoint ? withImages(interrupted.checkpoint) : result.messages);
|
|
995
|
+
await put('cancel', (await this.cancelFrame(sessionId)));
|
|
616
996
|
return;
|
|
617
997
|
}
|
|
618
998
|
// Writeback only while this stream still owns the session (Python `_stream_writeback_is_current`).
|
|
619
999
|
let current;
|
|
620
1000
|
try {
|
|
621
|
-
current = deps.store.get(sessionId);
|
|
1001
|
+
current = (await deps.store.get(sessionId));
|
|
622
1002
|
}
|
|
623
1003
|
catch {
|
|
624
1004
|
current = s;
|
|
@@ -654,15 +1034,15 @@ export class TurnRunner {
|
|
|
654
1034
|
await this.steerRewrites.get(streamId);
|
|
655
1035
|
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, str(result.pending_steer), 'followup');
|
|
656
1036
|
followUp = leftovers;
|
|
657
|
-
this.persistError(s, streamId, classification.label, payload, activeTurnToken, resultMessages, result.provider);
|
|
1037
|
+
await this.persistError(s, settlement, streamId, classification.label, payload, activeTurnToken, resultMessages, result.provider);
|
|
658
1038
|
// TAL-512: a btw error carries no session: its journaled frame must not keep a copy of the parent conversation.
|
|
659
1039
|
if (!opts.ephemeral)
|
|
660
|
-
payload.session = this.publicTerminalSession(s);
|
|
1040
|
+
payload.session = (await this.publicTerminalSession(s));
|
|
661
1041
|
payload.session_id = s.session_id;
|
|
662
1042
|
payload.old_session_id = sessionId;
|
|
663
1043
|
for (const [event, data] of steerEvents)
|
|
664
|
-
put(event, data);
|
|
665
|
-
put('apperror', payload);
|
|
1044
|
+
(await put(event, data));
|
|
1045
|
+
await put('apperror', payload);
|
|
666
1046
|
return;
|
|
667
1047
|
}
|
|
668
1048
|
// TAL-512: a btw turn passes the failure check above first, so its last assistant row is this turn's reply.
|
|
@@ -676,11 +1056,11 @@ export class TurnRunner {
|
|
|
676
1056
|
}
|
|
677
1057
|
}
|
|
678
1058
|
succeeded = true;
|
|
679
|
-
opts.onDone?.(answer);
|
|
1059
|
+
await opts.onDone?.(answer);
|
|
680
1060
|
// Python `_ephemeral_session_payload`: only role and content leave the server for a btw turn.
|
|
681
|
-
put('done', { session: { session_id: sessionId, messages: resultMessages.map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
|
|
1061
|
+
await put('done', { session: { session_id: sessionId, messages: resultMessages.map((m) => ({ role: m.role, content: m.content })) }, usage: { input_tokens: 0, output_tokens: 0 }, ephemeral: true, answer, terminal_state: answer.trim() ? 'completed' : 'no_response' });
|
|
682
1062
|
try {
|
|
683
|
-
|
|
1063
|
+
await deps.store.delete(sessionId, { tombstone: false });
|
|
684
1064
|
}
|
|
685
1065
|
catch { /* ignore */ }
|
|
686
1066
|
deps.store.sessions.delete(sessionId);
|
|
@@ -707,8 +1087,9 @@ export class TurnRunner {
|
|
|
707
1087
|
const { events: steerEvents, leftovers } = this.finalizeSteers(streamId, result.pending_steer, 'followup');
|
|
708
1088
|
s.messages = withoutToolImages(mergeDisplayMessagesAfterAgentResult(previousMessages, previousContext, resultMessages, msgText, { source: opts.source ?? 'webui', activeTurnToken, now: deps.now(), turnId: streamId, attachments: opts.attachments ?? [] }));
|
|
709
1089
|
s.context_messages = dedupeContext(context);
|
|
1090
|
+
// The settled turn's own figures: a compaction it waited on (TAL-1005) installed an estimate its prompt now supersedes.
|
|
1091
|
+
s.post_compression_context_tokens_estimate = result.compressed ? estimate : null;
|
|
710
1092
|
if (result.compressed) {
|
|
711
|
-
s.post_compression_context_tokens_estimate = estimate;
|
|
712
1093
|
// TAL-540: the anchor is the last visible row before this turn (never its eager checkpoint); the merged transcript
|
|
713
1094
|
// holds no compaction marker, so the summary is the model context's newest one.
|
|
714
1095
|
const before = visibleMessagesForAnchor(previousMessages.filter((m) => m._turn_id !== streamId));
|
|
@@ -733,20 +1114,11 @@ export class TurnRunner {
|
|
|
733
1114
|
s.threshold_tokens = result.context.threshold_tokens;
|
|
734
1115
|
if (typeof result.context.last_prompt_tokens === 'number')
|
|
735
1116
|
s.last_prompt_tokens = result.context.last_prompt_tokens;
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
...meteringUsage(s), context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
740
|
-
post_compression_context_tokens_estimate: s.post_compression_context_tokens_estimate, ...contextUsage(s, () => deps.service().deps.contextLengthFor(s.model, s.model_provider)),
|
|
741
|
-
};
|
|
742
|
-
put('compressed', { session_id: sessionId, old_session_id: sessionId, new_session_id: sessionId, continuation_session_id: sessionId, message: 'Compression finished', usage });
|
|
743
|
-
}
|
|
1117
|
+
s.next_turn_threshold_tokens = result.context.next_turn_threshold_tokens ?? null;
|
|
1118
|
+
if (result.compressed)
|
|
1119
|
+
(await put('compressed', this.compressedFrame(s)));
|
|
744
1120
|
s.tool_calls = extractToolCallsFromMessages(s.messages, liveToolCalls, s.tool_calls);
|
|
745
|
-
s
|
|
746
|
-
s.pending_user_message = null;
|
|
747
|
-
s.pending_attachments = [];
|
|
748
|
-
s.pending_started_at = null;
|
|
749
|
-
s.pending_user_source = null;
|
|
1121
|
+
this.clearPendingTurn(s);
|
|
750
1122
|
// TAL-576: a turn that ran means the provider has usable credentials again.
|
|
751
1123
|
s.process_wakeup_pause = null;
|
|
752
1124
|
const attachments = opts.attachments ?? [];
|
|
@@ -801,18 +1173,18 @@ export class TurnRunner {
|
|
|
801
1173
|
this.persistConsumedSteers(s, streamId, previousStartedAt(s, activeRun), now);
|
|
802
1174
|
// TAL-493: the rows this turn's Agent wrote to state.db are now in the transcript, so the merge must not replay them
|
|
803
1175
|
// (all of them, the dropped summary request too).
|
|
804
|
-
|
|
805
|
-
|
|
1176
|
+
this.settleStateDb(s, streamId, agentRows, { previousContext, read: settlement });
|
|
1177
|
+
recordBackgroundReview(s, streamId);
|
|
1178
|
+
await deps.store.save(s);
|
|
806
1179
|
deps.pending.clearApprovals(sessionId);
|
|
807
1180
|
deps.pending.clearClarifies(sessionId);
|
|
808
1181
|
deps.events.publish('session_done', { profile: s.profile, sessionId: s.session_id });
|
|
809
1182
|
// Python `_persistent_state_changes`: toast memory/skill files the agent wrote during the turn.
|
|
810
1183
|
try {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
put('state_saved', { session_id: sessionId, kind: 'skill', action: change.action, name: change.name });
|
|
1184
|
+
if (persistentChanges?.memory_saved)
|
|
1185
|
+
(await put('state_saved', { session_id: sessionId, kind: 'memory', action: 'saved' }));
|
|
1186
|
+
for (const change of persistentChanges?.skills ?? [])
|
|
1187
|
+
(await put('state_saved', { session_id: sessionId, kind: 'skill', action: change.action, name: change.name }));
|
|
816
1188
|
}
|
|
817
1189
|
catch (error) {
|
|
818
1190
|
deps.log(`[webui] persistent state change detection failed for ${sessionId}: ${error.message}`);
|
|
@@ -828,7 +1200,7 @@ export class TurnRunner {
|
|
|
828
1200
|
cache_hit_percent: cacheHit(cacheReadTokens, inputTokens), turn_cache_hit_percent: cacheHit(turnCacheRead, turnInput), duration_seconds: Math.round(duration * 1000) / 1000,
|
|
829
1201
|
context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
830
1202
|
};
|
|
831
|
-
const doneSession = this.publicTerminalSession(s);
|
|
1203
|
+
const doneSession = (await this.publicTerminalSession(s));
|
|
832
1204
|
// TAL-299: the ring's figures match the terminal session's (and a detail reload's).
|
|
833
1205
|
for (const key of CONTEXT_USAGE_FIELDS)
|
|
834
1206
|
doneUsage[key] = doneSession[key];
|
|
@@ -847,58 +1219,80 @@ export class TurnRunner {
|
|
|
847
1219
|
if (result.tool_limit_reached)
|
|
848
1220
|
donePayload.terminal_reason = 'max_iterations';
|
|
849
1221
|
// TAL-397: the live meter ends on the persisted counters and turn rate.
|
|
850
|
-
put('metering', { session_id: sessionId, usage: meteringUsage(s), tps: doneUsage.tps ?? null, tps_available: doneUsage.tps !== undefined, estimated: false });
|
|
851
|
-
put('done', donePayload);
|
|
1222
|
+
await put('metering', { session_id: sessionId, usage: meteringUsage(s), tps: doneUsage.tps ?? null, tps_available: doneUsage.tps !== undefined, estimated: false });
|
|
1223
|
+
await put('done', donePayload);
|
|
852
1224
|
for (const [event, payload] of steerEvents)
|
|
853
|
-
put(event, payload);
|
|
1225
|
+
(await put(event, payload));
|
|
854
1226
|
// The turn is over: release admission before the title work so a follow-up message is accepted while the
|
|
855
1227
|
// (up to two) title prompts run; the channel and journal stay open for the `title` events (Python retired the
|
|
856
1228
|
// worker before its daemon-thread title generation).
|
|
857
1229
|
this.registry.activeRuns.delete(streamId);
|
|
858
1230
|
this.settledStreams.add(streamId);
|
|
859
|
-
|
|
1231
|
+
// TAL-1005: a turn that settled at or above the next turn's threshold starts the compaction now, so the next message
|
|
1232
|
+
// usually finds the session compacted; a follow-up turn admitted meanwhile joins it.
|
|
1233
|
+
void deps.service().ensureCompacted(sessionId);
|
|
1234
|
+
await this.startSteerFollowUp(sessionId, leftovers);
|
|
860
1235
|
// Before the title work, so a generated title's own state.db sync lands after this one's provisional title.
|
|
861
1236
|
await deps.syncUsage?.(s, usedModel || null);
|
|
862
1237
|
// Title work and the goal judge run side by side; `stream_end` follows both, so their frames reach the stream.
|
|
863
1238
|
const work = await Promise.allSettled([this.backgroundTitle(s, put), this.continueGoal(s, streamId, put)]);
|
|
864
|
-
put('stream_end', { session_id: sessionId });
|
|
1239
|
+
await put('stream_end', { session_id: sessionId });
|
|
865
1240
|
for (const r of work)
|
|
866
1241
|
if (r.status === 'rejected')
|
|
867
1242
|
throw r.reason;
|
|
868
1243
|
succeeded = true;
|
|
869
|
-
opts.onDone?.(lastAnswer(s.messages));
|
|
1244
|
+
await opts.onDone?.(lastAnswer(s.messages));
|
|
870
1245
|
}
|
|
871
1246
|
catch (error) {
|
|
1247
|
+
timing?.fail(error);
|
|
1248
|
+
await streamWrites;
|
|
1249
|
+
const readOnly = !agentDispatched ? (await deps.service().readOnlyReason(s)) : null;
|
|
1250
|
+
if (readOnly) {
|
|
1251
|
+
timing?.fail({ status: 403 });
|
|
1252
|
+
ownershipRefused = true;
|
|
1253
|
+
if (this.registry.writebackOwners.get(sessionId) === streamId && s.active_stream_id === streamId) {
|
|
1254
|
+
// Cancellation persists the prompt and a Stop row; an unstarted, refused turn must add neither.
|
|
1255
|
+
s.messages = s.messages.filter((m) => !(m.role === 'user' && m._turn_id === streamId));
|
|
1256
|
+
this.clearPendingTurn(s);
|
|
1257
|
+
if (isDict(s.extra.completion_acceptances))
|
|
1258
|
+
s.extra.completion_acceptances = Object.fromEntries(Object.entries(s.extra.completion_acceptances).filter(([, value]) => value !== streamId));
|
|
1259
|
+
await deps.store.save(s, { touchUpdatedAt: false });
|
|
1260
|
+
}
|
|
1261
|
+
await put('apperror', { type: 'error', terminal_state: 'error', message: readOnly, session_id: sessionId, session: (await this.publicTerminalSession(s)) });
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
872
1264
|
if (settledAt.value && !(error instanceof SidecarError)) {
|
|
873
1265
|
deps.log(`[webui] ERROR settling turn ${streamId}\n${error instanceof Error ? (error.stack ?? error.message) : String(error)}`);
|
|
874
1266
|
}
|
|
1267
|
+
// TAL-1162: the error's settlement read runs on the worker before the gates below decide.
|
|
1268
|
+
const settlement = await deps.service().settlementRead(s);
|
|
875
1269
|
if (this.registry.cancelled.has(streamId)) {
|
|
876
|
-
this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
877
|
-
put('cancel', this.cancelFrame(sessionId));
|
|
1270
|
+
await this.finalizeCancelled(s, streamId, opts.ephemeral);
|
|
1271
|
+
await put('cancel', (await this.cancelFrame(sessionId)));
|
|
878
1272
|
return;
|
|
879
1273
|
}
|
|
880
1274
|
const message = error instanceof Error ? error.message : String(error);
|
|
881
1275
|
const condition = error instanceof SidecarError ? error.condition : undefined;
|
|
882
|
-
const
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
1276
|
+
const runtime = error instanceof SidecarError && ['sidecar_unavailable', 'agent_runtime_stale', 'agent_incompatible'].includes(error.condition)
|
|
1277
|
+
? runtimeFailureDetails(error.condition, error.data) : null;
|
|
1278
|
+
const classification = runtime ? { label: 'Error', type: runtime.type, hint: '' } : classifyProviderError(message, { condition });
|
|
1279
|
+
const payload = providerErrorPayload(runtime?.message ?? message, classification.type, classification.hint, deps.redactEnabled());
|
|
1280
|
+
if (runtime)
|
|
1281
|
+
Object.assign(payload, runtime);
|
|
888
1282
|
try {
|
|
889
1283
|
let current = s;
|
|
890
1284
|
try {
|
|
891
|
-
current = deps.store.get(sessionId);
|
|
1285
|
+
current = (await deps.store.get(sessionId));
|
|
892
1286
|
}
|
|
893
1287
|
catch {
|
|
894
1288
|
current = s;
|
|
895
1289
|
}
|
|
896
1290
|
if (current.active_stream_id === streamId) {
|
|
897
1291
|
for (const [event, data] of this.takeSteerEventsBefore(streamId, 'apperror'))
|
|
898
|
-
put(event, data);
|
|
899
|
-
this.persistError(current, streamId, classification.label, payload, activeTurnToken);
|
|
1292
|
+
(await put(event, data));
|
|
1293
|
+
await this.persistError(current, settlement, streamId, classification.label, payload, activeTurnToken);
|
|
900
1294
|
if (!opts.ephemeral)
|
|
901
|
-
payload.session = this.publicTerminalSession(current);
|
|
1295
|
+
payload.session = (await this.publicTerminalSession(current));
|
|
902
1296
|
}
|
|
903
1297
|
}
|
|
904
1298
|
catch (persistError) {
|
|
@@ -906,15 +1300,20 @@ export class TurnRunner {
|
|
|
906
1300
|
}
|
|
907
1301
|
payload.session_id = sessionId;
|
|
908
1302
|
payload.old_session_id = sessionId;
|
|
909
|
-
put('apperror', payload);
|
|
1303
|
+
await put('apperror', payload);
|
|
910
1304
|
}
|
|
911
1305
|
finally {
|
|
912
|
-
this.teardown(sessionId, streamId);
|
|
1306
|
+
await this.teardown(sessionId, streamId);
|
|
913
1307
|
// After persistence and teardown: a background route's failure cleanup deletes the hidden session, which must
|
|
914
1308
|
// not race the error writeback above (a re-saved session would resurface in the sidebar).
|
|
915
1309
|
if (!succeeded)
|
|
916
|
-
opts.onFailed?.(failure);
|
|
917
|
-
|
|
1310
|
+
await opts.onFailed?.(failure);
|
|
1311
|
+
// TAL-1005: a Stop or an error after the turn ran on a compaction's result left that job's estimate under a context
|
|
1312
|
+
// its prompt and work may have grown; the idle trigger judges the context measured again.
|
|
1313
|
+
if (!succeeded && !ownershipRefused && !opts.ephemeral)
|
|
1314
|
+
void deps.service().remeasureContext(sessionId).then((measured) => { if (measured)
|
|
1315
|
+
void deps.service().ensureCompacted(sessionId); });
|
|
1316
|
+
await this.startSteerFollowUp(sessionId, followUp);
|
|
918
1317
|
}
|
|
919
1318
|
}
|
|
920
1319
|
/**
|
|
@@ -995,33 +1394,30 @@ export class TurnRunner {
|
|
|
995
1394
|
return images ? [{ type: 'text', text: withFiles() }, ...parts] : withFiles();
|
|
996
1395
|
}
|
|
997
1396
|
/** The settled session as a terminal frame carries it: the display window, redacted. */
|
|
998
|
-
publicTerminalSession(s) {
|
|
999
|
-
return
|
|
1397
|
+
async publicTerminalSession(s) {
|
|
1398
|
+
return this.deps.service().windowedDetail(s, TRANSCRIPT_WINDOW);
|
|
1000
1399
|
}
|
|
1001
|
-
/**
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
attachTodoState(payload, s.messages, s.extra[UNSETTLED_TODO_KEY]);
|
|
1018
|
-
payload.tool_calls = toolCallsForMessageWindow(s.tool_calls, offset, limited.length);
|
|
1019
|
-
payload.compression_reference = compressionReference(s, scened);
|
|
1020
|
-
return payload;
|
|
1400
|
+
/** 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`. */
|
|
1401
|
+
compressedFrame(s) {
|
|
1402
|
+
const usage = {
|
|
1403
|
+
...meteringUsage(s), context_length: s.context_length ?? 0, threshold_tokens: s.threshold_tokens ?? 0, last_prompt_tokens: s.last_prompt_tokens ?? 0,
|
|
1404
|
+
post_compression_context_tokens_estimate: s.post_compression_context_tokens_estimate, ...contextUsage(s, () => this.deps.service().deps.contextLengthFor(s.model, s.model_provider)),
|
|
1405
|
+
};
|
|
1406
|
+
const sid = s.session_id;
|
|
1407
|
+
return { session_id: sid, old_session_id: sid, new_session_id: sid, continuation_session_id: sid, message: 'Compression finished', usage };
|
|
1408
|
+
}
|
|
1409
|
+
/** Release only the pending admission metadata; transcript settlement belongs to the caller. */
|
|
1410
|
+
clearPendingTurn(s) {
|
|
1411
|
+
s.active_stream_id = null;
|
|
1412
|
+
s.pending_user_message = null;
|
|
1413
|
+
s.pending_attachments = [];
|
|
1414
|
+
s.pending_started_at = null;
|
|
1415
|
+
s.pending_user_source = null;
|
|
1021
1416
|
}
|
|
1022
1417
|
/** Python `_materialize_pending_user_turn_before_error` + error message append + save. */
|
|
1023
1418
|
/** `runtimeProvider`: the provider the Agent resolved for the turn, when it reported one. */
|
|
1024
|
-
|
|
1419
|
+
/** `read`: the settlement's one state.db read, taken before the caller's gates decided (TAL-1162). */
|
|
1420
|
+
async persistError(s, read, streamId, label, payload, activeTurnToken, agentRows = [], runtimeProvider = '') {
|
|
1025
1421
|
const startedAt = s.pending_started_at;
|
|
1026
1422
|
// TAL-576: an automatic wakeup with no usable credentials pauses the next ones instead of failing each.
|
|
1027
1423
|
if (s.pending_user_source === 'process_wakeup' && payload.type === 'credential_pool_empty') {
|
|
@@ -1038,11 +1434,7 @@ export class TurnRunner {
|
|
|
1038
1434
|
}
|
|
1039
1435
|
this.materializePendingUserTurn(s, activeTurnToken, streamId);
|
|
1040
1436
|
const duration = typeof startedAt === 'number' && startedAt > 0 ? Math.max(0, this.deps.now() - startedAt) : null;
|
|
1041
|
-
s
|
|
1042
|
-
s.pending_user_message = null;
|
|
1043
|
-
s.pending_attachments = [];
|
|
1044
|
-
s.pending_started_at = null;
|
|
1045
|
-
s.pending_user_source = null;
|
|
1437
|
+
this.clearPendingTurn(s);
|
|
1046
1438
|
this.appendPartialSnapshot(s, streamId);
|
|
1047
1439
|
const hint = str(payload.hint);
|
|
1048
1440
|
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 };
|
|
@@ -1068,14 +1460,12 @@ export class TurnRunner {
|
|
|
1068
1460
|
this.persistConsumedSteers(s, streamId, startedAt, this.deps.now());
|
|
1069
1461
|
// TAL-921: the next prompt remembers the turn's work; an exhausted compression's work would only overflow again. One
|
|
1070
1462
|
// read builds the context and settles, so a row committed in between is never covered without reaching either.
|
|
1071
|
-
const read = this.deps.service().stateDbRead(s);
|
|
1072
1463
|
const context = payload.type === 'compression_exhausted' ? null : this.unsettledContext(streamId, agentRows, read);
|
|
1073
1464
|
if (context)
|
|
1074
1465
|
s.context_messages = context;
|
|
1075
|
-
|
|
1076
|
-
this.deps.service().settleStateDb(s, { turnId: streamId, previousContext: stop?.previousContext ?? [], agentRows, startId: stop?.stateDbStartId ?? null, read });
|
|
1466
|
+
this.settleStateDb(s, streamId, agentRows, { read });
|
|
1077
1467
|
try {
|
|
1078
|
-
this.deps.store.save(s);
|
|
1468
|
+
(await this.deps.store.save(s));
|
|
1079
1469
|
}
|
|
1080
1470
|
catch (error) {
|
|
1081
1471
|
this.deps.log(`[webui] WARNING: failed to save error turn for ${s.session_id}: ${error.message}`);
|
|
@@ -1086,6 +1476,20 @@ export class TurnRunner {
|
|
|
1086
1476
|
this.deps.pending.clearClarifies(s.session_id);
|
|
1087
1477
|
this.deps.events.publish('session_error', { profile: s.profile, sessionId: s.session_id });
|
|
1088
1478
|
}
|
|
1479
|
+
/** The model history a turn starts from: the session's context merged with `stateRows`, without the turn's own rows. */
|
|
1480
|
+
turnHistory(s, streamId, stateRows) {
|
|
1481
|
+
// TAL-578: an eager save already put this turn's prompt in the transcript; `user_message` carries it, so the history
|
|
1482
|
+
// leaves it out.
|
|
1483
|
+
const previousContext = structuredClone(this.deps.service().modelContext(s, stateRows)).filter((m) => m._turn_id !== streamId);
|
|
1484
|
+
// Python `_sanitize_messages_for_agent`: the model never sees display-only rows or a replayed cancelled prompt, and
|
|
1485
|
+
// the Agent gets back the exact bytes it sent (TAL-541).
|
|
1486
|
+
return { previousContext, apiHistory: sanitizeMessagesForApi(previousContext, { preserveApiContent: true, issuedReasoningSince: s.reasoning_replay_from }) };
|
|
1487
|
+
}
|
|
1488
|
+
/** Every terminal path carries the same starting read's id and generation into settlement. */
|
|
1489
|
+
settleStateDb(s, streamId, agentRows, opts) {
|
|
1490
|
+
const stop = this.stopContexts.get(streamId);
|
|
1491
|
+
this.deps.service().settleStateDb(s, { turnId: streamId, previousContext: stop?.previousContext ?? opts.previousContext ?? [], agentRows, startId: stop?.stateDbStartId ?? null, startGeneration: stop?.stateDbStartGeneration ?? null, read: opts.read });
|
|
1492
|
+
}
|
|
1089
1493
|
/**
|
|
1090
1494
|
* TAL-921: the model context a turn that ends without its settled result leaves: the work its Agent reported, else the
|
|
1091
1495
|
* rows it committed past its starting read in `read`, the read its settlement uses.
|
|
@@ -1094,7 +1498,7 @@ export class TurnRunner {
|
|
|
1094
1498
|
const stop = this.stopContexts.get(streamId);
|
|
1095
1499
|
if (!stop)
|
|
1096
1500
|
return null;
|
|
1097
|
-
const after = stop.stateDbStartId;
|
|
1501
|
+
const after = stop.stateDbStartGeneration !== null && read.generation !== null && stop.stateDbStartGeneration !== read.generation ? -1 : stop.stateDbStartId;
|
|
1098
1502
|
const stateRows = after === null ? [] : read.rows.filter((m) => typeof m._state_db_row_id === 'number' && m._state_db_row_id > after);
|
|
1099
1503
|
return this.deps.service().unsettledTurnContext({ ...stop, streamedText: (this.registry.partialText.get(streamId) ?? []).join(''), agentRows, stateRows });
|
|
1100
1504
|
}
|
|
@@ -1143,11 +1547,11 @@ export class TurnRunner {
|
|
|
1143
1547
|
* Python `_emit_cancel_event`: the terminal frame carries the settled session so tabs can render the partial without a
|
|
1144
1548
|
* reload. TAL-364: no copy — clients show their one localized status for the `cancelled` outcome.
|
|
1145
1549
|
*/
|
|
1146
|
-
cancelFrame(sessionId) {
|
|
1550
|
+
async cancelFrame(sessionId) {
|
|
1147
1551
|
let snapshot = null;
|
|
1148
1552
|
if (sessionId)
|
|
1149
1553
|
try {
|
|
1150
|
-
snapshot = this.publicTerminalSession(this.deps.store.get(sessionId));
|
|
1554
|
+
snapshot = (await this.publicTerminalSession((await this.deps.store.get(sessionId))));
|
|
1151
1555
|
}
|
|
1152
1556
|
catch {
|
|
1153
1557
|
snapshot = null;
|
|
@@ -1159,21 +1563,33 @@ export class TurnRunner {
|
|
|
1159
1563
|
* turn's model context keeps what the Agent had captured (`checkpoint`, its canonical transcript) and the prose that
|
|
1160
1564
|
* streamed, so the next request continues from it; the terminal row carries the outcome and no copy.
|
|
1161
1565
|
*/
|
|
1162
|
-
finalizeCancelled(s, streamId, ephemeral, checkpoint = null) {
|
|
1566
|
+
async finalizeCancelled(s, streamId, ephemeral, checkpoint = null) {
|
|
1163
1567
|
if (this.registry.writebackOwners.get(s.session_id) !== streamId)
|
|
1164
1568
|
return false;
|
|
1165
1569
|
let current = s;
|
|
1166
1570
|
try {
|
|
1167
|
-
current = this.deps.store.get(s.session_id);
|
|
1571
|
+
current = (await this.deps.store.get(s.session_id));
|
|
1168
1572
|
}
|
|
1169
1573
|
catch {
|
|
1170
1574
|
return false;
|
|
1171
1575
|
}
|
|
1576
|
+
// TAL-921: one state.db read builds the context and settles the turn. TAL-1162: it runs on the state.db worker, before
|
|
1577
|
+
// any gate below decides.
|
|
1578
|
+
const read = await this.deps.service().settlementRead(current);
|
|
1579
|
+
if (this.registry.writebackOwners.get(s.session_id) !== streamId)
|
|
1580
|
+
return false;
|
|
1172
1581
|
if (current.active_stream_id !== null && current.active_stream_id !== streamId)
|
|
1173
1582
|
return false;
|
|
1583
|
+
const stop = this.stopContexts.get(streamId);
|
|
1584
|
+
if (stop?.startReadPending) {
|
|
1585
|
+
// TAL-1162: the Stop landed before the turn's starting read did, so the Agent never ran: every row this later read
|
|
1586
|
+
// holds predates the turn, and the context the turn would have started from keeps them. No row is its own work.
|
|
1587
|
+
const { previousContext, apiHistory } = this.turnHistory(current, streamId, read.rows);
|
|
1588
|
+
Object.assign(stop, { previousContext, historyLength: apiHistory.length, startReadPending: false });
|
|
1589
|
+
}
|
|
1174
1590
|
if (ephemeral) {
|
|
1175
1591
|
try {
|
|
1176
|
-
|
|
1592
|
+
await this.deps.store.delete(current.session_id, { tombstone: false });
|
|
1177
1593
|
}
|
|
1178
1594
|
catch { /* ignore */ }
|
|
1179
1595
|
this.deps.store.sessions.delete(current.session_id);
|
|
@@ -1185,9 +1601,9 @@ export class TurnRunner {
|
|
|
1185
1601
|
if (quiet && current.active_stream_id === null) {
|
|
1186
1602
|
// TAL-493: rows its worker committed while unwinding are covered by what it reports; others are kept.
|
|
1187
1603
|
if (checkpoint) {
|
|
1188
|
-
this.
|
|
1604
|
+
this.settleStateDb(current, streamId, checkpoint, { read });
|
|
1189
1605
|
try {
|
|
1190
|
-
this.deps.store.save(current);
|
|
1606
|
+
(await this.deps.store.save(current));
|
|
1191
1607
|
}
|
|
1192
1608
|
catch {
|
|
1193
1609
|
return false;
|
|
@@ -1195,10 +1611,8 @@ export class TurnRunner {
|
|
|
1195
1611
|
}
|
|
1196
1612
|
return true;
|
|
1197
1613
|
}
|
|
1198
|
-
const stop = this.stopContexts.get(streamId);
|
|
1199
1614
|
const canonical = stop !== undefined && checkpoint !== null && checkpointTurnStart(checkpoint, stop.msgText, stop.historyLength) !== null;
|
|
1200
|
-
|
|
1201
|
-
const settle = (read) => {
|
|
1615
|
+
const settle = () => {
|
|
1202
1616
|
if (!stop)
|
|
1203
1617
|
return null;
|
|
1204
1618
|
if (canonical)
|
|
@@ -1212,13 +1626,12 @@ export class TurnRunner {
|
|
|
1212
1626
|
const ownStop = current.messages.some((m) => isCancelMarker(m) && m._turn_id === streamId);
|
|
1213
1627
|
// TAL-493: the worker's rows committed after the first settlement are covered by what it reports; others are kept.
|
|
1214
1628
|
if (ownStop && checkpoint) {
|
|
1215
|
-
const
|
|
1216
|
-
const late = canonical && !stop.checkpointed ? settle(read) : null;
|
|
1629
|
+
const late = canonical && !stop.checkpointed ? settle() : null;
|
|
1217
1630
|
if (late)
|
|
1218
1631
|
current.context_messages = dedupeContext(late);
|
|
1219
|
-
this.
|
|
1632
|
+
this.settleStateDb(current, streamId, checkpoint, { read });
|
|
1220
1633
|
try {
|
|
1221
|
-
this.deps.store.save(current);
|
|
1634
|
+
(await this.deps.store.save(current));
|
|
1222
1635
|
}
|
|
1223
1636
|
catch {
|
|
1224
1637
|
return false;
|
|
@@ -1228,24 +1641,19 @@ export class TurnRunner {
|
|
|
1228
1641
|
}
|
|
1229
1642
|
const startedAt = current.pending_started_at;
|
|
1230
1643
|
this.materializePendingUserTurn(current, buildActiveTurnToken(streamId, current.pending_started_at), streamId);
|
|
1231
|
-
const
|
|
1232
|
-
const context = settle(read);
|
|
1644
|
+
const context = settle();
|
|
1233
1645
|
if (context)
|
|
1234
1646
|
current.context_messages = dedupeContext(context);
|
|
1235
|
-
current
|
|
1236
|
-
current.pending_user_message = null;
|
|
1237
|
-
current.pending_attachments = [];
|
|
1238
|
-
current.pending_started_at = null;
|
|
1239
|
-
current.pending_user_source = null;
|
|
1647
|
+
this.clearPendingTurn(current);
|
|
1240
1648
|
if (!quiet) {
|
|
1241
1649
|
this.appendPartialSnapshot(current, streamId);
|
|
1242
1650
|
current.messages.push({ role: 'assistant', content: '', _error: true, _terminal_state: 'cancelled', timestamp: Math.trunc(this.deps.now()), _turn_id: streamId });
|
|
1243
1651
|
}
|
|
1244
1652
|
this.keepLiveState(current, streamId);
|
|
1245
1653
|
this.persistConsumedSteers(current, streamId, startedAt, this.deps.now());
|
|
1246
|
-
this.
|
|
1654
|
+
this.settleStateDb(current, streamId, checkpoint, { read });
|
|
1247
1655
|
try {
|
|
1248
|
-
this.deps.store.save(current);
|
|
1656
|
+
(await this.deps.store.save(current));
|
|
1249
1657
|
}
|
|
1250
1658
|
catch {
|
|
1251
1659
|
return false;
|
|
@@ -1257,8 +1665,8 @@ export class TurnRunner {
|
|
|
1257
1665
|
return true;
|
|
1258
1666
|
}
|
|
1259
1667
|
/** Push a frame onto the session's live chat stream from outside the turn (Python gateway notify callbacks). */
|
|
1260
|
-
emitToSession(sessionId, event, data) {
|
|
1261
|
-
this.sessionPuts.get(sessionId)?.(event, data);
|
|
1668
|
+
async emitToSession(sessionId, event, data) {
|
|
1669
|
+
await this.sessionPuts.get(sessionId)?.(event, data);
|
|
1262
1670
|
}
|
|
1263
1671
|
/** TAL-397: a turn that ends without its result keeps what the live stream already showed: counters and todo list. */
|
|
1264
1672
|
keepLiveState(s, streamId) {
|
|
@@ -1269,13 +1677,15 @@ export class TurnRunner {
|
|
|
1269
1677
|
if (todos)
|
|
1270
1678
|
s.extra[UNSETTLED_TODO_KEY] = todos;
|
|
1271
1679
|
}
|
|
1272
|
-
teardown(sessionId, streamId) {
|
|
1680
|
+
async teardown(sessionId, streamId) {
|
|
1681
|
+
this.runStatuses.delete(streamId);
|
|
1273
1682
|
this.liveUsage.delete(streamId);
|
|
1683
|
+
this.compressionSpans.delete(streamId);
|
|
1274
1684
|
this.liveTodos.delete(streamId);
|
|
1275
1685
|
const writer = this.writers.get(streamId);
|
|
1276
1686
|
if (writer) {
|
|
1277
1687
|
try {
|
|
1278
|
-
writer.close();
|
|
1688
|
+
(await writer.close());
|
|
1279
1689
|
}
|
|
1280
1690
|
catch { /* ignore */ }
|
|
1281
1691
|
this.writers.delete(streamId);
|
|
@@ -1304,21 +1714,28 @@ export class TurnRunner {
|
|
|
1304
1714
|
catch { /* best effort */ }
|
|
1305
1715
|
}
|
|
1306
1716
|
/** Journal files with a live writer (retention must leave them alone). */
|
|
1307
|
-
activeJournalPaths() {
|
|
1308
|
-
return new Set([...this.writers.values()].map((w) => this.deps.journal.pathFor(w.sessionId, w.runId)));
|
|
1309
|
-
}
|
|
1310
1717
|
// ── title ────────────────────────────────────────────────────────────────
|
|
1311
1718
|
/** Python `_generate_llm_session_title_via_aux`: aux-route statuses (`llm_aux`, `llm_aux_retry`, `llm_*_aux`). */
|
|
1312
1719
|
async llmTitle(s, userText, assistantText) {
|
|
1313
1720
|
const sidecar = this.deps.sidecar();
|
|
1314
1721
|
if (!sidecar)
|
|
1315
1722
|
return { title: '', status: 'llm_error_aux', rawPreview: '' };
|
|
1723
|
+
const service = this.deps.service();
|
|
1724
|
+
await service.deps.warmSessionModelRepair?.(s.profile);
|
|
1725
|
+
const [model, provider] = service.turnModel(s);
|
|
1316
1726
|
const [qa, prompts] = titlePrompts(userText, assistantText);
|
|
1727
|
+
const source = [...s.messages, ...s.context_messages, ...await service.stateDbRows(s)];
|
|
1317
1728
|
let status = 'llm_error_aux';
|
|
1318
1729
|
for (const [idx, prompt] of prompts.entries()) {
|
|
1730
|
+
if ((await this.deps.service().isReadOnly(s)))
|
|
1731
|
+
return { title: '', status: 'read_only', rawPreview: '' };
|
|
1319
1732
|
let raw = '';
|
|
1320
1733
|
try {
|
|
1321
|
-
const result = await sidecar.call('aux.complete', {
|
|
1734
|
+
const result = await sidecar.call('aux.complete', {
|
|
1735
|
+
profile_home: this.deps.profileHome(s.profile), task: 'title_generation',
|
|
1736
|
+
messages: withDataUseRequirements([{ role: 'system', content: prompt }, { role: 'user', content: qa }], source), max_tokens: 64,
|
|
1737
|
+
main_runtime: { model: model ?? '', provider }, main_fallback: true,
|
|
1738
|
+
});
|
|
1322
1739
|
raw = str(result.text);
|
|
1323
1740
|
}
|
|
1324
1741
|
catch (error) {
|
|
@@ -1339,6 +1756,43 @@ export class TurnRunner {
|
|
|
1339
1756
|
}
|
|
1340
1757
|
return { title: '', status, rawPreview: '' };
|
|
1341
1758
|
}
|
|
1759
|
+
/**
|
|
1760
|
+
* TAL-902: `/refine [focus]` starts the Agent's memory/skill review of the chat's model context on its agent
|
|
1761
|
+
* (`chat.refine`, the agent a turn would build); no turn starts. Without a chat, while a turn runs, or before anything
|
|
1762
|
+
* was said it answers with the Agent's wording instead.
|
|
1763
|
+
*/
|
|
1764
|
+
async refine(s, focus) {
|
|
1765
|
+
if (!s)
|
|
1766
|
+
return 'Nothing to refine yet — send a message first.';
|
|
1767
|
+
const context = await this.deps.service().readModelContext(s);
|
|
1768
|
+
if (s.active_stream_id)
|
|
1769
|
+
return 'Agent is running — wait for the turn to finish, then /refine.';
|
|
1770
|
+
const messages = sanitizeMessagesForApi(context, { preserveApiContent: true, issuedReasoningSince: s.reasoning_replay_from });
|
|
1771
|
+
if (!messages.length)
|
|
1772
|
+
return 'Nothing to refine yet — the conversation is empty.';
|
|
1773
|
+
// Like a turn: no agent is built in a profile whose home is going away.
|
|
1774
|
+
if (this.deps.profileDeleting?.(s.profile ?? null))
|
|
1775
|
+
throw new HttpFailure(409, `Profile '${str(s.profile)}' is being deleted.`);
|
|
1776
|
+
const sidecar = this.deps.sidecar();
|
|
1777
|
+
if (!sidecar)
|
|
1778
|
+
throw new SidecarError('The Agent sidecar is not running; /refine is unavailable until it starts.', { condition: 'sidecar_unavailable' });
|
|
1779
|
+
const [model, provider] = this.deps.service().turnModel(s);
|
|
1780
|
+
const cfg = (await this.deps.profileConfig?.(s.profile ?? null)) ?? {};
|
|
1781
|
+
await this.deps.service().assertWritable(s);
|
|
1782
|
+
return (await sidecar.call('chat.refine', {
|
|
1783
|
+
profile_home: this.deps.profileHome(s.profile), session_id: s.session_id, messages, focus, model: model ?? '', model_provider: provider,
|
|
1784
|
+
enabled_toolsets: this.deps.toolsetsFor(s), ...agentBuildParams(cfg, 'webui', provider, this.deps.env ?? {}),
|
|
1785
|
+
...this.agentWorkspace(s, (await this.deps.service().turnWorkspace(s))),
|
|
1786
|
+
}, { timeoutMs: 120_000 })).output;
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* TAL-903: the workspace a session's agent is built at, and whether it is a host path (the profile's terminal runs
|
|
1790
|
+
* locally). Only then does the Agent anchor AGENTS.md discovery and its memory provider there; `chat.start` and
|
|
1791
|
+
* `chat.refine` send the same pair, so the agent `/refine` builds is the one the next turn reuses.
|
|
1792
|
+
*/
|
|
1793
|
+
agentWorkspace(s, workspace) {
|
|
1794
|
+
return { workspace, workspace_local: this.deps.workspaces.profileSupportsLocalIo(s.profile) };
|
|
1795
|
+
}
|
|
1342
1796
|
/**
|
|
1343
1797
|
* Python `generate_session_title_for_session`: on-demand title from the persisted transcript; never touches
|
|
1344
1798
|
* `llm_title_generated`. An explicit regenerate runs even with `auxiliary.title_generation.enabled` off (TAL-531).
|
|
@@ -1352,6 +1806,7 @@ export class TurnRunner {
|
|
|
1352
1806
|
let rawPreview = '';
|
|
1353
1807
|
if (assistantText)
|
|
1354
1808
|
({ title: next, status: llmStatus, rawPreview } = await this.llmTitle(s, userText, assistantText));
|
|
1809
|
+
await this.deps.service().assertWritable(s);
|
|
1355
1810
|
if (next)
|
|
1356
1811
|
return { title: next, status: llmStatus, rawPreview };
|
|
1357
1812
|
const fallback = fallbackTitleFromExchange(userText, assistantText);
|
|
@@ -1366,7 +1821,7 @@ export class TurnRunner {
|
|
|
1366
1821
|
async titleClaimed(s) {
|
|
1367
1822
|
if (!looksLikeDefaultCliTitle({ title: s.title, source_tag: s.source_tag, raw_source: s.raw_source, session_source: s.session_source, source_label: s.source_label }))
|
|
1368
1823
|
return;
|
|
1369
|
-
await this.backgroundTitle(s, () =>
|
|
1824
|
+
await this.backgroundTitle(s, () => Promise.resolve()).catch((error) => { this.deps.log(`[webui] imported session title failed for ${s.session_id}: ${error.message}`); });
|
|
1370
1825
|
}
|
|
1371
1826
|
/** Python `_run_background_title_update` (spawned only when `_background_title_generation_inputs` is eligible). */
|
|
1372
1827
|
async backgroundTitle(s, put) {
|
|
@@ -1378,8 +1833,8 @@ export class TurnRunner {
|
|
|
1378
1833
|
const eligible = ['Untitled', 'New Chat', ''].includes(placeholder) || placeholder === titleFrom(s.messages, '') || invalidExisting || defaultTitle(s);
|
|
1379
1834
|
const [userText, assistantText] = firstExchangeSnippets(s.messages);
|
|
1380
1835
|
// Python `_put_title_status`: empty reason/title/raw_preview keys are omitted.
|
|
1381
|
-
const status = (status, reason = '', title = '', rawPreview = '') => {
|
|
1382
|
-
put('title_status', { session_id: sessionId, status, ...(reason ? { reason } : {}), ...(title ? { title } : {}), ...(rawPreview ? { raw_preview: rawPreview } : {}) });
|
|
1836
|
+
const status = async (status, reason = '', title = '', rawPreview = '') => {
|
|
1837
|
+
await put('title_status', { session_id: sessionId, status, ...(reason ? { reason } : {}), ...(title ? { title } : {}), ...(rawPreview ? { raw_preview: rawPreview } : {}) });
|
|
1383
1838
|
};
|
|
1384
1839
|
// Python ran the adaptive refresh only when the first-title job was not eligible.
|
|
1385
1840
|
if (!eligible || (s.llm_title_generated && !invalidExisting) || !userText || !assistantText) {
|
|
@@ -1387,21 +1842,25 @@ export class TurnRunner {
|
|
|
1387
1842
|
return;
|
|
1388
1843
|
}
|
|
1389
1844
|
if (s.manual_title) {
|
|
1390
|
-
status('skipped', 'manual_title', placeholder);
|
|
1845
|
+
(await status('skipped', 'manual_title', placeholder));
|
|
1391
1846
|
return;
|
|
1392
1847
|
}
|
|
1393
1848
|
// A missing config.yaml reads as `{}` (enabled); an unreadable one cannot confirm the user has not opted out.
|
|
1394
1849
|
const cfg = await this.deps.profileConfig?.(s.profile ?? null);
|
|
1395
1850
|
if (!cfg) {
|
|
1396
|
-
status('skipped', 'config_unavailable', placeholder);
|
|
1851
|
+
(await status('skipped', 'config_unavailable', placeholder));
|
|
1397
1852
|
return;
|
|
1398
1853
|
}
|
|
1399
1854
|
if (!titleGenerationEnabled(cfg)) {
|
|
1400
|
-
status('skipped', 'title_generation_disabled', placeholder);
|
|
1855
|
+
(await status('skipped', 'title_generation_disabled', placeholder));
|
|
1401
1856
|
return;
|
|
1402
1857
|
}
|
|
1403
1858
|
const generated = await this.llmTitle(s, userText, assistantText);
|
|
1404
1859
|
const { status: llmStatus, rawPreview } = generated;
|
|
1860
|
+
if (llmStatus === 'read_only') {
|
|
1861
|
+
(await status('skipped', 'read_only', placeholder));
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1405
1864
|
let next = generated.title;
|
|
1406
1865
|
let source = llmStatus;
|
|
1407
1866
|
if (!next) {
|
|
@@ -1414,26 +1873,30 @@ export class TurnRunner {
|
|
|
1414
1873
|
const fallbackReason = source === 'fallback' && llmStatus ? `local_summary:${llmStatus}` : 'local_summary';
|
|
1415
1874
|
let current = s;
|
|
1416
1875
|
try {
|
|
1417
|
-
current = this.deps.store.get(sessionId);
|
|
1876
|
+
current = (await this.deps.store.get(sessionId));
|
|
1418
1877
|
}
|
|
1419
1878
|
catch {
|
|
1420
1879
|
// A session deleted while the title prompt ran stays deleted.
|
|
1421
|
-
if (this.deps.store.wasDeleted(sessionId))
|
|
1880
|
+
if ((await this.deps.store.wasDeleted(sessionId)))
|
|
1422
1881
|
return;
|
|
1423
1882
|
current = s;
|
|
1424
1883
|
}
|
|
1884
|
+
if ((await this.deps.service().isReadOnly(current))) {
|
|
1885
|
+
(await status('skipped', 'read_only', str(current.title).trim()));
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1425
1888
|
let effective = str(current.title).trim();
|
|
1426
1889
|
let wrote = false;
|
|
1427
1890
|
if (next) {
|
|
1428
1891
|
const stillAuto = effective === placeholder || ['Untitled', 'New Chat', ''].includes(effective) || effective === titleFrom(current.messages, '') || looksInvalidGeneratedTitle(current.title) || defaultTitle(current);
|
|
1429
1892
|
if (current.manual_title || !stillAuto) {
|
|
1430
|
-
status('skipped', 'manual_title', effective);
|
|
1893
|
+
(await status('skipped', 'manual_title', effective));
|
|
1431
1894
|
return;
|
|
1432
1895
|
}
|
|
1433
1896
|
if (next !== effective) {
|
|
1434
1897
|
current.title = next;
|
|
1435
1898
|
markSessionTitleGenerated(current);
|
|
1436
|
-
this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1899
|
+
await this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1437
1900
|
// Python `sync_session_title` after generation: the state.db row follows when `sync_to_insights` is on.
|
|
1438
1901
|
await this.deps.syncTitle?.(current);
|
|
1439
1902
|
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
@@ -1442,11 +1905,11 @@ export class TurnRunner {
|
|
|
1442
1905
|
}
|
|
1443
1906
|
}
|
|
1444
1907
|
if (wrote) {
|
|
1445
|
-
status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
|
|
1446
|
-
put('title', { session_id: sessionId, title: effective });
|
|
1908
|
+
await status(source, source === 'fallback' ? fallbackReason : llmStatus, effective, rawPreview);
|
|
1909
|
+
await put('title', { session_id: sessionId, title: effective });
|
|
1447
1910
|
}
|
|
1448
1911
|
else
|
|
1449
|
-
status('skipped', source || 'unchanged', effective, rawPreview);
|
|
1912
|
+
(await status('skipped', source || 'unchanged', effective, rawPreview));
|
|
1450
1913
|
}
|
|
1451
1914
|
/**
|
|
1452
1915
|
* Python `_maybe_schedule_title_refresh` + `_run_background_title_refresh`: every N exchanges a generated title is
|
|
@@ -1463,7 +1926,7 @@ export class TurnRunner {
|
|
|
1463
1926
|
// Admission is released before title work, so an earlier turn's refresh may still be running: refreshes apply in
|
|
1464
1927
|
// turn order and each reads the title its predecessor wrote.
|
|
1465
1928
|
const sessionId = s.session_id;
|
|
1466
|
-
const run = (this.titleRefreshes.get(sessionId) ?? Promise.resolve()).catch(() => undefined).then(() => this.applyTitleRefresh(sessionId, s, userText, assistantText, put, status));
|
|
1929
|
+
const run = (this.titleRefreshes.get(sessionId) ?? Promise.resolve()).catch(() => undefined).then(async () => { await this.applyTitleRefresh(sessionId, s, userText, assistantText, put, status); });
|
|
1467
1930
|
this.titleRefreshes.set(sessionId, run);
|
|
1468
1931
|
try {
|
|
1469
1932
|
await run;
|
|
@@ -1474,44 +1937,48 @@ export class TurnRunner {
|
|
|
1474
1937
|
}
|
|
1475
1938
|
}
|
|
1476
1939
|
async applyTitleRefresh(sessionId, s, userText, assistantText, put, status) {
|
|
1477
|
-
const read = () => { try {
|
|
1478
|
-
return this.deps.store.get(sessionId);
|
|
1940
|
+
const read = async () => { try {
|
|
1941
|
+
return (await this.deps.store.get(sessionId));
|
|
1479
1942
|
}
|
|
1480
1943
|
catch {
|
|
1481
1944
|
return s;
|
|
1482
1945
|
} };
|
|
1483
|
-
const latest = read();
|
|
1946
|
+
const latest = (await read());
|
|
1484
1947
|
const placeholder = str(latest.title).trim();
|
|
1485
1948
|
if (['Untitled', 'New Chat', ''].includes(placeholder) || latest.manual_title || !latest.llm_title_generated)
|
|
1486
1949
|
return;
|
|
1487
1950
|
const cfg = await this.deps.profileConfig?.(latest.profile ?? null);
|
|
1488
1951
|
if (!cfg || !titleGenerationEnabled(cfg)) {
|
|
1489
|
-
status('refresh_skipped', cfg ? 'title_generation_disabled' : 'config_unavailable', placeholder);
|
|
1952
|
+
(await status('refresh_skipped', cfg ? 'title_generation_disabled' : 'config_unavailable', placeholder));
|
|
1490
1953
|
return;
|
|
1491
1954
|
}
|
|
1492
1955
|
const { title: next, status: llmStatus, rawPreview } = await this.llmTitle(latest, userText, assistantText);
|
|
1493
1956
|
if (!next) {
|
|
1494
|
-
status('refresh_skipped', llmStatus || 'empty', placeholder, rawPreview);
|
|
1957
|
+
(await status('refresh_skipped', llmStatus || 'empty', placeholder, rawPreview));
|
|
1495
1958
|
return;
|
|
1496
1959
|
}
|
|
1497
1960
|
const normalize = (value) => value.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1498
1961
|
if (normalize(next) === normalize(placeholder)) {
|
|
1499
|
-
status('refresh_skipped', 'same_title', placeholder, rawPreview);
|
|
1962
|
+
(await status('refresh_skipped', 'same_title', placeholder, rawPreview));
|
|
1500
1963
|
return;
|
|
1501
1964
|
}
|
|
1502
|
-
const current = read();
|
|
1965
|
+
const current = (await read());
|
|
1503
1966
|
// The user may have renamed the session while the title prompt ran.
|
|
1504
1967
|
if (current.manual_title || str(current.title).trim() !== placeholder) {
|
|
1505
|
-
status('skipped', 'manual_title', str(current.title).trim());
|
|
1968
|
+
(await status('skipped', 'manual_title', str(current.title).trim()));
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
if ((await this.deps.service().isReadOnly(current))) {
|
|
1972
|
+
(await status('refresh_skipped', 'read_only', str(current.title).trim()));
|
|
1506
1973
|
return;
|
|
1507
1974
|
}
|
|
1508
1975
|
current.title = next;
|
|
1509
1976
|
markSessionTitleGenerated(current);
|
|
1510
|
-
this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1977
|
+
await this.deps.store.save(current, { touchUpdatedAt: false });
|
|
1511
1978
|
await this.deps.syncTitle?.(current);
|
|
1512
1979
|
this.deps.events.publish('title', { profile: current.profile, sessionId });
|
|
1513
|
-
status('refreshed', llmStatus, next, rawPreview);
|
|
1514
|
-
put('title', { session_id: sessionId, title: next });
|
|
1980
|
+
await status('refreshed', llmStatus, next, rawPreview);
|
|
1981
|
+
await put('title', { session_id: sessionId, title: next });
|
|
1515
1982
|
}
|
|
1516
1983
|
// ── cancel / steer ───────────────────────────────────────────────────────
|
|
1517
1984
|
/**
|
|
@@ -1519,6 +1986,26 @@ export class TurnRunner {
|
|
|
1519
1986
|
* `steer_withdrawn` (stopped) this Stop emitted, for the cancel response (TAL-426).
|
|
1520
1987
|
*/
|
|
1521
1988
|
async cancel(streamId) {
|
|
1989
|
+
const ownerCancelled = await this.deps.service().owner?.cancel(streamId);
|
|
1990
|
+
if (ownerCancelled !== null && ownerCancelled !== undefined)
|
|
1991
|
+
return { cancelled: ownerCancelled, withdrawn: [] };
|
|
1992
|
+
// TAL-899: the user's Stop holds automatic turns, even one that lands after `done`, unless a newer turn already
|
|
1993
|
+
// took the session: a late or retried Stop for a turn the user followed holds nothing.
|
|
1994
|
+
const owner = this.registry.ownerSessionId(streamId) ?? this.registry.activeRuns.get(streamId)?.session_id;
|
|
1995
|
+
if (owner && this.registry.writebackOwners.get(owner) === streamId) {
|
|
1996
|
+
this.stopHeld.add(owner);
|
|
1997
|
+
try {
|
|
1998
|
+
const session = (await this.deps.store.get(owner));
|
|
1999
|
+
session.automatic_turns_stop_held = true;
|
|
2000
|
+
await this.deps.store.save(session, { touchUpdatedAt: false });
|
|
2001
|
+
}
|
|
2002
|
+
catch (error) {
|
|
2003
|
+
this.deps.log(`[webui] WARNING: failed to persist Stop hold for session ${owner}; interrupting with the in-memory hold: ${error.message}`);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
return (await this.stop(streamId));
|
|
2007
|
+
}
|
|
2008
|
+
async stop(streamId) {
|
|
1522
2009
|
const channel = this.registry.peek(streamId);
|
|
1523
2010
|
const run = this.registry.activeRuns.get(streamId);
|
|
1524
2011
|
if (!channel && !run)
|
|
@@ -1553,24 +2040,24 @@ export class TurnRunner {
|
|
|
1553
2040
|
if (sessionId) {
|
|
1554
2041
|
let current = null;
|
|
1555
2042
|
try {
|
|
1556
|
-
current = this.deps.store.get(sessionId);
|
|
2043
|
+
current = (await this.deps.store.get(sessionId));
|
|
1557
2044
|
}
|
|
1558
2045
|
catch {
|
|
1559
2046
|
current = null;
|
|
1560
2047
|
}
|
|
1561
2048
|
if (current?.active_stream_id === streamId) {
|
|
1562
|
-
this.finalizeCancelled(current, streamId, run?.ephemeral, checkpoint);
|
|
2049
|
+
await this.finalizeCancelled(current, streamId, run?.ephemeral, checkpoint);
|
|
1563
2050
|
}
|
|
1564
2051
|
}
|
|
1565
2052
|
// The worker may already have written its terminal row while the interrupt was in flight; the stream is then
|
|
1566
2053
|
// gone from the registry and this path must not add a second one.
|
|
1567
2054
|
if (channel && this.registry.streams.has(streamId)) {
|
|
1568
2055
|
const writer = this.writers.get(streamId) ?? null;
|
|
1569
|
-
const emit = (event, data) => {
|
|
2056
|
+
const emit = async (event, data) => {
|
|
1570
2057
|
let eventId = null;
|
|
1571
2058
|
if (writer) {
|
|
1572
2059
|
try {
|
|
1573
|
-
eventId = writer.appendSseEvent(event, data).event_id;
|
|
2060
|
+
eventId = (await writer.appendSseEvent(event, data)).event_id;
|
|
1574
2061
|
}
|
|
1575
2062
|
catch {
|
|
1576
2063
|
eventId = null;
|
|
@@ -1579,8 +2066,10 @@ export class TurnRunner {
|
|
|
1579
2066
|
channel.put([event, data, eventId]);
|
|
1580
2067
|
};
|
|
1581
2068
|
for (const [event, data] of steerEvents)
|
|
1582
|
-
emit(event, data);
|
|
1583
|
-
|
|
2069
|
+
(await emit(event, data));
|
|
2070
|
+
this.runStatuses.delete(streamId);
|
|
2071
|
+
await emit('run_status', { session_id: sessionId, status: null });
|
|
2072
|
+
await emit('cancel', (await this.cancelFrame(sessionId)));
|
|
1584
2073
|
this.registry.streams.delete(streamId);
|
|
1585
2074
|
this.registry.liveIds.delete(streamId);
|
|
1586
2075
|
}
|
|
@@ -1596,7 +2085,7 @@ export class TurnRunner {
|
|
|
1596
2085
|
const streamId = this.registry.activeRunStreamForSession(sessionId);
|
|
1597
2086
|
if (!streamId || this.registry.activeRuns.get(streamId)?.origin !== 'background')
|
|
1598
2087
|
return false;
|
|
1599
|
-
await this.
|
|
2088
|
+
await this.stop(streamId);
|
|
1600
2089
|
let timer;
|
|
1601
2090
|
await Promise.race([this.workers.get(streamId), new Promise((resolve) => { timer = setTimeout(resolve, BACKGROUND_UNWIND_WAIT_MS); })]);
|
|
1602
2091
|
clearTimeout(timer);
|
|
@@ -1605,7 +2094,7 @@ export class TurnRunner {
|
|
|
1605
2094
|
async steer(sessionId, text, displayText, steerId) {
|
|
1606
2095
|
let s = null;
|
|
1607
2096
|
try {
|
|
1608
|
-
s = this.deps.store.get(sessionId, { metadataOnly: true });
|
|
2097
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
1609
2098
|
}
|
|
1610
2099
|
catch {
|
|
1611
2100
|
s = null;
|
|
@@ -1624,7 +2113,7 @@ export class TurnRunner {
|
|
|
1624
2113
|
return { accepted: false, fallback: 'no_cached_agent', stream_id: null };
|
|
1625
2114
|
// Register provisionally before the RPC: the Agent may apply the steer and finish the turn before the steer
|
|
1626
2115
|
// reply arrives, and the turn's finalisation must then already see the record. A rejection removes it again.
|
|
1627
|
-
return this.withSteerChain(activeStreamId, () => this.steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId));
|
|
2116
|
+
return this.withSteerChain(activeStreamId, sessionId, () => this.steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId));
|
|
1628
2117
|
}
|
|
1629
2118
|
async steerInto(sidecar, sessionId, activeStreamId, text, displayText, steerId) {
|
|
1630
2119
|
const record = { steer_id: steerId, session_id: sessionId, stream_id: activeStreamId, text, display_text: displayText || text, created_at: this.deps.now(), state: 'pending', can_redirect: false };
|
|
@@ -1656,7 +2145,7 @@ export class TurnRunner {
|
|
|
1656
2145
|
record.can_redirect = result.can_redirect === true;
|
|
1657
2146
|
// TAL-424: every client shows it until the Agent takes it (unless it already did while the reply was in flight).
|
|
1658
2147
|
if (this.steers.get(activeStreamId)?.includes(record))
|
|
1659
|
-
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
2148
|
+
(await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record)));
|
|
1660
2149
|
}
|
|
1661
2150
|
return { accepted: result.accepted, fallback: result.accepted ? null : (result.fallback ?? 'not_running'), stream_id: activeStreamId, steer_id: steerId };
|
|
1662
2151
|
}
|
|
@@ -1725,10 +2214,10 @@ export class TurnRunner {
|
|
|
1725
2214
|
return boundary;
|
|
1726
2215
|
}
|
|
1727
2216
|
/** A steer is saved as it enters the stream, so a reload mid-turn already has it; the terminal save re-places it. */
|
|
1728
|
-
saveConsumedSteers(sessionId, streamId) {
|
|
2217
|
+
async saveConsumedSteers(sessionId, streamId) {
|
|
1729
2218
|
let s;
|
|
1730
2219
|
try {
|
|
1731
|
-
s = this.deps.store.get(sessionId);
|
|
2220
|
+
s = (await this.deps.store.get(sessionId));
|
|
1732
2221
|
}
|
|
1733
2222
|
catch {
|
|
1734
2223
|
return;
|
|
@@ -1736,7 +2225,7 @@ export class TurnRunner {
|
|
|
1736
2225
|
if (s.active_stream_id !== streamId || this.placeConsumedSteers(s, streamId, s.pending_started_at) === null)
|
|
1737
2226
|
return;
|
|
1738
2227
|
try {
|
|
1739
|
-
this.deps.store.save(s, { touchUpdatedAt: false });
|
|
2228
|
+
(await this.deps.store.save(s, { touchUpdatedAt: false }));
|
|
1740
2229
|
}
|
|
1741
2230
|
catch (error) {
|
|
1742
2231
|
this.deps.log(`[webui] WARNING: failed to save consumed steer for ${sessionId}: ${error.message}`);
|
|
@@ -1796,18 +2285,18 @@ export class TurnRunner {
|
|
|
1796
2285
|
* TAL-424: the steers a finished turn never took go on as one follow-up turn, sent once by the server for every client.
|
|
1797
2286
|
* Should another turn already hold the session, they steer into that one instead, so no text is lost.
|
|
1798
2287
|
*/
|
|
1799
|
-
startSteerFollowUp(sessionId, texts) {
|
|
2288
|
+
async startSteerFollowUp(sessionId, texts) {
|
|
1800
2289
|
if (!texts.length)
|
|
1801
2290
|
return;
|
|
1802
2291
|
let s;
|
|
1803
2292
|
try {
|
|
1804
|
-
s = this.deps.store.get(sessionId);
|
|
2293
|
+
s = (await this.deps.store.get(sessionId));
|
|
1805
2294
|
}
|
|
1806
2295
|
catch {
|
|
1807
2296
|
return;
|
|
1808
2297
|
}
|
|
1809
2298
|
const msg = texts.join('\n');
|
|
1810
|
-
const started = this.
|
|
2299
|
+
const started = (await this.startServerTurn(s, { msg }));
|
|
1811
2300
|
if (!started._status || started._status < 400)
|
|
1812
2301
|
return;
|
|
1813
2302
|
if (started._status === 409 && started.active_stream_id) {
|
|
@@ -1835,61 +2324,88 @@ export class TurnRunner {
|
|
|
1835
2324
|
// Only a known goal turn shows progress up front; any other turn shows nothing unless a goal turns out to be active.
|
|
1836
2325
|
const goalTurn = this.registry.goalRelated.has(streamId);
|
|
1837
2326
|
if (goalTurn)
|
|
1838
|
-
put('goal', { session_id: sessionId, state: 'evaluating', message: 'Evaluating goal progress…', message_key: 'goal_evaluating_progress' });
|
|
2327
|
+
(await put('goal', { session_id: sessionId, state: 'evaluating', message: 'Evaluating goal progress…', message_key: 'goal_evaluating_progress' }));
|
|
1839
2328
|
let decision;
|
|
1840
2329
|
try {
|
|
1841
|
-
|
|
2330
|
+
// The workspace the turn ran in, unrecovered: the Agent pauses the goal rather than run its gates anywhere else.
|
|
2331
|
+
await this.deps.service().assertWritable(s);
|
|
2332
|
+
decision = await sidecar.call('goals.evaluate', { session_id: sessionId, profile_home: this.deps.profileHome(s.profile), last_response: lastResponse, user_initiated: true, workspace: str(s.workspace) || undefined });
|
|
1842
2333
|
}
|
|
1843
2334
|
catch (error) {
|
|
1844
2335
|
this.deps.log(`[webui] WARNING: goal evaluation failed for ${sessionId}: ${error.message}`);
|
|
1845
2336
|
if (goalTurn)
|
|
1846
|
-
put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
2337
|
+
(await put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' }));
|
|
1847
2338
|
return;
|
|
1848
2339
|
}
|
|
2340
|
+
// The judgement moved the goal on (turns, park, pause, done); an inactive one only matters if a goal still shows.
|
|
2341
|
+
if (decision.verdict !== 'inactive' || this.goals.view(sessionId))
|
|
2342
|
+
await this.refreshGoal(s, put);
|
|
1849
2343
|
if (!goalTurn && decision.verdict === 'inactive')
|
|
1850
2344
|
return;
|
|
1851
2345
|
const prompt = str(decision.continuation_prompt).trim();
|
|
1852
2346
|
const proceed = decision.should_continue && Boolean(prompt);
|
|
1853
2347
|
const status = { message: decision.message, message_key: decision.message_key, message_args: decision.message_args ?? [], decision: decision.verdict };
|
|
1854
|
-
|
|
1855
|
-
|
|
2348
|
+
if (!proceed) {
|
|
2349
|
+
(await put('goal', { session_id: sessionId, state: 'idle', ...status }));
|
|
1856
2350
|
return;
|
|
2351
|
+
}
|
|
1857
2352
|
let current;
|
|
1858
2353
|
try {
|
|
1859
|
-
current = this.deps.store.get(sessionId);
|
|
2354
|
+
current = (await this.deps.store.get(sessionId));
|
|
1860
2355
|
}
|
|
1861
2356
|
catch {
|
|
1862
2357
|
return;
|
|
1863
2358
|
}
|
|
1864
|
-
const started = this.
|
|
2359
|
+
const started = (await this.startServerTurn(current, { msg: prompt, goalRelated: true }));
|
|
2360
|
+
// TAL-899: a Stop, even one after the answer, holds the continuation; the user's next turn is judged instead.
|
|
2361
|
+
if (started.held) {
|
|
2362
|
+
(await put('goal', { session_id: sessionId, state: 'idle', decision: 'held', message: '' }));
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
await put('goal', { session_id: sessionId, state: 'continuing', ...status });
|
|
1865
2366
|
if (started._status === 409 && started.active_stream_id) {
|
|
1866
2367
|
this.registry.goalRelated.add(started.active_stream_id);
|
|
1867
2368
|
return;
|
|
1868
2369
|
}
|
|
1869
2370
|
if (started._status !== undefined && started._status >= 400) {
|
|
1870
2371
|
this.deps.log(`[webui] WARNING: goal continuation for ${sessionId} not started (${String(started._status)}): ${str(started.error)}`);
|
|
1871
|
-
put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
2372
|
+
await put('goal', { session_id: sessionId, state: 'idle', decision: 'error', message: '' });
|
|
1872
2373
|
return;
|
|
1873
2374
|
}
|
|
1874
|
-
put('goal_continue', { session_id: sessionId, state: 'continuing', continuation_prompt: prompt, text: prompt, stream_id: str(started.stream_id), ...status });
|
|
2375
|
+
await put('goal_continue', { session_id: sessionId, state: 'continuing', continuation_prompt: prompt, text: prompt, stream_id: str(started.stream_id), ...status });
|
|
1875
2376
|
}
|
|
1876
2377
|
/** The Agent's pending steer text settles which steers it has taken: those become `steer_consumed` and their rows. */
|
|
1877
|
-
settleConsumedSteers(sessionId, streamId, pendingText) {
|
|
2378
|
+
async settleConsumedSteers(sessionId, streamId, pendingText) {
|
|
1878
2379
|
for (const record of this.takeConsumedSteers(streamId, pendingText, { keepLeftovers: true }))
|
|
1879
|
-
this.emitToSession(sessionId, 'steer_consumed', record);
|
|
1880
|
-
this.saveConsumedSteers(sessionId, streamId);
|
|
2380
|
+
(await this.emitToSession(sessionId, 'steer_consumed', record));
|
|
2381
|
+
await this.saveConsumedSteers(sessionId, streamId);
|
|
1881
2382
|
}
|
|
1882
2383
|
/** What every client shows for a pending steer; the server decides which actions it offers. */
|
|
1883
2384
|
pendingSteerView(r) {
|
|
1884
2385
|
const open = r.state === 'pending';
|
|
1885
2386
|
return { steer_id: r.steer_id, text: r.display_text, submitted_at: r.created_at, state: r.state, actions: { edit: open, cancel: open, send_now: open && r.can_redirect } };
|
|
1886
2387
|
}
|
|
2388
|
+
liveToolProgress(streamId) {
|
|
2389
|
+
if (this.registry.cancelled.has(streamId))
|
|
2390
|
+
return [];
|
|
2391
|
+
return (this.registry.liveToolCalls.get(streamId) ?? []).filter((call) => !call.done && call.progress).map((call) => ({ ...call, id: str(call.tid) }));
|
|
2392
|
+
}
|
|
2393
|
+
runStatus(streamId) {
|
|
2394
|
+
return this.runStatuses.get(streamId) ?? null;
|
|
2395
|
+
}
|
|
2396
|
+
/** TAL-1006: the start (epoch seconds) of the stream's open compression span, else null. */
|
|
2397
|
+
compressionStartedAt(streamId) {
|
|
2398
|
+
return this.compressionSpans.get(streamId) ?? null;
|
|
2399
|
+
}
|
|
1887
2400
|
/** TAL-424: the active stream's pending steers, oldest first (session detail `pending_steers`). */
|
|
1888
2401
|
pendingSteers(streamId) {
|
|
1889
2402
|
return (this.steers.get(streamId) ?? []).map((r) => this.pendingSteerView(r));
|
|
1890
2403
|
}
|
|
1891
|
-
withSteerChain(streamId, fn) {
|
|
1892
|
-
const run = (this.steerChains.get(streamId) ?? Promise.resolve()).then(
|
|
2404
|
+
withSteerChain(streamId, sessionId, fn) {
|
|
2405
|
+
const run = (this.steerChains.get(streamId) ?? Promise.resolve()).then(async () => {
|
|
2406
|
+
await this.deps.service().getForMutation(sessionId);
|
|
2407
|
+
return fn();
|
|
2408
|
+
});
|
|
1893
2409
|
const settled = run.catch(() => undefined).then(() => { if (this.steerChains.get(streamId) === settled)
|
|
1894
2410
|
this.steerChains.delete(streamId); });
|
|
1895
2411
|
this.steerChains.set(streamId, settled);
|
|
@@ -1903,7 +2419,7 @@ export class TurnRunner {
|
|
|
1903
2419
|
async rewriteSteer(sessionId, steerId, fallback, rewrite) {
|
|
1904
2420
|
let s = null;
|
|
1905
2421
|
try {
|
|
1906
|
-
s = this.deps.store.get(sessionId, { metadataOnly: true });
|
|
2422
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
1907
2423
|
}
|
|
1908
2424
|
catch {
|
|
1909
2425
|
s = null;
|
|
@@ -1911,21 +2427,21 @@ export class TurnRunner {
|
|
|
1911
2427
|
const streamId = s?.active_stream_id;
|
|
1912
2428
|
const sidecar = this.deps.sidecar();
|
|
1913
2429
|
if (!streamId || !sidecar)
|
|
1914
|
-
return fallback;
|
|
1915
|
-
return this.withSteerChain(streamId, async () => {
|
|
2430
|
+
return fallback('not_running');
|
|
2431
|
+
return this.withSteerChain(streamId, sessionId, async () => {
|
|
1916
2432
|
const records = this.steers.get(streamId) ?? [];
|
|
1917
2433
|
const index = records.findIndex((r) => r.steer_id === steerId);
|
|
1918
2434
|
if (index < 0 || records[index].state !== 'pending')
|
|
1919
|
-
return fallback;
|
|
2435
|
+
return fallback('taken');
|
|
1920
2436
|
// In flight before the sidecar call, so a pending-text report racing its reply is deferred too.
|
|
1921
2437
|
let finished = () => undefined;
|
|
1922
2438
|
this.steerRewrites.set(streamId, new Promise((resolve) => { finished = resolve; }));
|
|
1923
|
-
let outcome = { result: fallback, changed: false };
|
|
2439
|
+
let outcome = { result: fallback('failed'), changed: false };
|
|
1924
2440
|
try {
|
|
1925
2441
|
outcome = await rewrite(sidecar, streamId, records, index);
|
|
1926
2442
|
}
|
|
1927
2443
|
catch {
|
|
1928
|
-
outcome = { result: fallback, changed: false };
|
|
2444
|
+
outcome = { result: fallback('failed'), changed: false };
|
|
1929
2445
|
}
|
|
1930
2446
|
finally {
|
|
1931
2447
|
this.steerRewrites.delete(streamId);
|
|
@@ -1933,7 +2449,7 @@ export class TurnRunner {
|
|
|
1933
2449
|
const deferred = this.deferredPendingSteer.get(streamId);
|
|
1934
2450
|
this.deferredPendingSteer.delete(streamId);
|
|
1935
2451
|
if (deferred !== undefined && !outcome.changed)
|
|
1936
|
-
this.settleConsumedSteers(sessionId, streamId, deferred);
|
|
2452
|
+
(await this.settleConsumedSteers(sessionId, streamId, deferred));
|
|
1937
2453
|
}
|
|
1938
2454
|
return outcome.result;
|
|
1939
2455
|
});
|
|
@@ -1948,54 +2464,61 @@ export class TurnRunner {
|
|
|
1948
2464
|
* TAL-424 Edit and Cancel: take a pending steer back before the Agent takes it. `withdrawn: false` (and nothing
|
|
1949
2465
|
* emitted) when it is unknown, already taken or being sent; a taken one then settles as consumed.
|
|
1950
2466
|
*/
|
|
1951
|
-
withdrawSteer(sessionId, steerId, reason) {
|
|
1952
|
-
return this.rewriteSteer(sessionId, steerId, { withdrawn: false }, async (sidecar, streamId, records, index) => {
|
|
2467
|
+
async withdrawSteer(sessionId, steerId, reason) {
|
|
2468
|
+
return (await this.rewriteSteer(sessionId, steerId, () => ({ withdrawn: false }), async (sidecar, streamId, records, index) => {
|
|
1953
2469
|
const record = records[index];
|
|
1954
2470
|
const reply = await sidecar.call('chat.steer_withdraw', { stream_id: streamId, pending: records.map((r) => r.text), index });
|
|
1955
2471
|
if (!reply.withdrawn)
|
|
1956
2472
|
return { result: { withdrawn: false }, changed: false };
|
|
1957
2473
|
this.dropSteer(streamId, record);
|
|
1958
|
-
this.emitToSession(sessionId, 'steer_withdrawn', { steer_id: record.steer_id, reason, text: record.display_text });
|
|
2474
|
+
await this.emitToSession(sessionId, 'steer_withdrawn', { steer_id: record.steer_id, reason, text: record.display_text });
|
|
1959
2475
|
return { result: { withdrawn: true, text: record.display_text }, changed: true };
|
|
1960
|
-
});
|
|
2476
|
+
}));
|
|
1961
2477
|
}
|
|
1962
2478
|
/**
|
|
1963
2479
|
* TAL-424 Send now: deliver a pending steer with the Agent's redirect. A model request restarts with it (consumed at
|
|
1964
2480
|
* once); during tools it goes last on the Agent's queue and stays `sending_now` until taken; with nothing live it
|
|
1965
2481
|
* stays pending.
|
|
1966
2482
|
*/
|
|
1967
|
-
sendSteerNow(sessionId, steerId) {
|
|
1968
|
-
return this.rewriteSteer(sessionId, steerId, { redirected: false }, async (sidecar, streamId, records, index) => {
|
|
2483
|
+
async sendSteerNow(sessionId, steerId) {
|
|
2484
|
+
return (await this.rewriteSteer(sessionId, steerId, (outcome) => ({ redirected: false, outcome }), async (sidecar, streamId, records, index) => {
|
|
1969
2485
|
const record = records[index];
|
|
1970
2486
|
if (!record.can_redirect)
|
|
1971
|
-
return { result: { redirected: false }, changed: false };
|
|
2487
|
+
return { result: { redirected: false, outcome: 'failed' }, changed: false };
|
|
1972
2488
|
const reply = await sidecar.call('chat.steer_now', { stream_id: streamId, pending: records.map((r) => r.text), index });
|
|
1973
2489
|
if (!reply.withdrawn)
|
|
1974
|
-
return { result: { redirected: false }, changed: false };
|
|
2490
|
+
return { result: { redirected: false, outcome: reply.reason === 'not_running' ? 'not_running' : 'taken' }, changed: false };
|
|
1975
2491
|
if (!reply.redirected) {
|
|
1976
2492
|
// The Agent's queue changed meanwhile, so it went back last: clients follow the new order.
|
|
1977
2493
|
if (reply.requeued === 'last') {
|
|
1978
2494
|
this.dropSteer(streamId, record);
|
|
1979
2495
|
records.push(record);
|
|
1980
|
-
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
2496
|
+
(await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record)));
|
|
1981
2497
|
}
|
|
1982
|
-
return { result: { redirected: false }, changed: true };
|
|
2498
|
+
return { result: { redirected: false, outcome: 'next_step' }, changed: true };
|
|
1983
2499
|
}
|
|
1984
2500
|
this.dropSteer(streamId, record);
|
|
1985
2501
|
if (reply.delivery === 'steer') {
|
|
1986
2502
|
record.state = 'sending_now';
|
|
1987
2503
|
records.push(record);
|
|
1988
|
-
this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
2504
|
+
await this.emitToSession(sessionId, 'steer_pending', this.pendingSteerView(record));
|
|
1989
2505
|
}
|
|
1990
2506
|
else {
|
|
1991
|
-
this.emitToSession(sessionId, 'steer_consumed', this.consumedSteerPayload(record, { redirected: true }));
|
|
1992
|
-
this.saveConsumedSteers(sessionId, streamId);
|
|
2507
|
+
await this.emitToSession(sessionId, 'steer_consumed', this.consumedSteerPayload(record, { redirected: true }));
|
|
2508
|
+
await this.saveConsumedSteers(sessionId, streamId);
|
|
1993
2509
|
}
|
|
1994
|
-
return { result: { redirected: true }, changed: true };
|
|
1995
|
-
});
|
|
2510
|
+
return { result: { redirected: true, outcome: 'delivered' }, changed: true };
|
|
2511
|
+
}));
|
|
1996
2512
|
}
|
|
1997
2513
|
// ── approvals / clarify ──────────────────────────────────────────────────
|
|
1998
2514
|
async respondApproval(sessionId, choice, approvalId, enableYolo) {
|
|
2515
|
+
if (await this.deps.service().owner?.isOwned(sessionId)) {
|
|
2516
|
+
if (enableYolo)
|
|
2517
|
+
throw new HttpFailure(403, 'Change approval policy through the owning Agent');
|
|
2518
|
+
await this.deps.service().owner.respond(sessionId, 'approval', approvalId, choice);
|
|
2519
|
+
return { ok: true, choice };
|
|
2520
|
+
}
|
|
2521
|
+
sessionId = (await this.deps.service().idForMutation(sessionId));
|
|
1999
2522
|
// Python `_enable_session_yolo_and_release_pending`: a stale card id while another approval is parked
|
|
2000
2523
|
// must not commit YOLO for the whole session — fail closed with the relay envelope instead.
|
|
2001
2524
|
if (enableYolo && approvalId && !this.deps.pending.hasApprovalId(sessionId, approvalId) && this.deps.pending.hasPendingApproval(sessionId)) {
|
|
@@ -2008,7 +2531,7 @@ export class TurnRunner {
|
|
|
2008
2531
|
const relayFailure = (message) => ({ ok: false, choice, error: `The Agent sidecar did not accept the approval (${message}); retry in a moment.`, _status: 503 });
|
|
2009
2532
|
let s = null;
|
|
2010
2533
|
try {
|
|
2011
|
-
s = this.deps.store.get(sessionId, { metadataOnly: true });
|
|
2534
|
+
s = (await this.deps.store.get(sessionId, { metadataOnly: true }));
|
|
2012
2535
|
}
|
|
2013
2536
|
catch {
|
|
2014
2537
|
s = null;
|
|
@@ -2050,7 +2573,7 @@ export class TurnRunner {
|
|
|
2050
2573
|
return relayFailure(str(error.message));
|
|
2051
2574
|
}
|
|
2052
2575
|
}
|
|
2053
|
-
this.deps.service().setYolo(sessionId, true);
|
|
2576
|
+
await this.deps.service().setYolo(sessionId, true);
|
|
2054
2577
|
return null;
|
|
2055
2578
|
});
|
|
2056
2579
|
if (failure)
|
|
@@ -2084,17 +2607,21 @@ export class TurnRunner {
|
|
|
2084
2607
|
return;
|
|
2085
2608
|
}
|
|
2086
2609
|
if (this.deps.pending.dropApprovals(sessionId, new Set(before.filter((id) => !live.has(id)))).length)
|
|
2087
|
-
this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId));
|
|
2610
|
+
(await this.emitToSession(sessionId, ...this.approvalHeadFrame(sessionId)));
|
|
2088
2611
|
}
|
|
2089
2612
|
withYoloLock(sessionId, fn) {
|
|
2090
|
-
const run = (this.yoloChains.get(sessionId) ?? Promise.resolve()).then(
|
|
2613
|
+
const run = (this.yoloChains.get(sessionId) ?? Promise.resolve()).then(async () => {
|
|
2614
|
+
sessionId = (await this.deps.service().idForMutation(sessionId));
|
|
2615
|
+
return fn();
|
|
2616
|
+
});
|
|
2091
2617
|
const settled = run.catch(() => undefined).then(() => { if (this.yoloChains.get(sessionId) === settled)
|
|
2092
2618
|
this.yoloChains.delete(sessionId); });
|
|
2093
2619
|
this.yoloChains.set(sessionId, settled);
|
|
2094
2620
|
return run;
|
|
2095
2621
|
}
|
|
2096
2622
|
/** Python `set_session_yolo_enabled(False)`: the sidecar must drop its per-session YOLO state before the local flag clears. */
|
|
2097
|
-
disableYolo(sessionId) {
|
|
2623
|
+
async disableYolo(sessionId) {
|
|
2624
|
+
sessionId = (await this.deps.service().idForMutation(sessionId));
|
|
2098
2625
|
return this.withYoloLock(sessionId, async () => {
|
|
2099
2626
|
const sidecar = this.deps.sidecar();
|
|
2100
2627
|
if (sidecar) {
|
|
@@ -2105,7 +2632,7 @@ export class TurnRunner {
|
|
|
2105
2632
|
return { ok: false, error: `The Agent sidecar did not disable YOLO (${str(error.message)}); retry in a moment.`, yolo_enabled: this.deps.service().yolo(sessionId).yolo_enabled === true, _status: 503 };
|
|
2106
2633
|
}
|
|
2107
2634
|
}
|
|
2108
|
-
return this.deps.service().setYolo(sessionId, false);
|
|
2635
|
+
return (await this.deps.service().setYolo(sessionId, false));
|
|
2109
2636
|
});
|
|
2110
2637
|
}
|
|
2111
2638
|
/**
|
|
@@ -2114,16 +2641,25 @@ export class TurnRunner {
|
|
|
2114
2641
|
* did not acknowledge (prompt retained).
|
|
2115
2642
|
*/
|
|
2116
2643
|
async respondClarify(sessionId, clarifyId, reply) {
|
|
2644
|
+
if (await this.deps.service().owner?.isOwned(sessionId)) {
|
|
2645
|
+
await this.deps.service().owner.respond(sessionId, 'clarify', clarifyId, reply);
|
|
2646
|
+
return { ok: true };
|
|
2647
|
+
}
|
|
2648
|
+
sessionId = (await this.deps.service().idForMutation(sessionId));
|
|
2117
2649
|
const entry = this.deps.pending.peekClarify(sessionId, clarifyId);
|
|
2118
2650
|
if (!entry)
|
|
2119
2651
|
return { ok: false, stale: true };
|
|
2120
|
-
|
|
2652
|
+
// TAL-887: a password-manager answer goes to the Agent and nowhere else; `null` (a decline) is its empty reply.
|
|
2653
|
+
const vault = isVaultPrompt(entry);
|
|
2654
|
+
if (vault && typeof reply === 'string')
|
|
2655
|
+
return { ok: false, invalid: true };
|
|
2656
|
+
const response = vault ? vaultReply(entry, reply) : reply === null ? null : clarifyReply(entry, reply);
|
|
2121
2657
|
if (response === null)
|
|
2122
2658
|
return { ok: false, invalid: true };
|
|
2123
2659
|
// A test-hook prompt has no parked Agent waiter: it is answered here.
|
|
2124
2660
|
if (entry._injected === true) {
|
|
2125
2661
|
this.deps.pending.resolveClarify(sessionId, str(entry.clarify_id));
|
|
2126
|
-
return { ok: true, response };
|
|
2662
|
+
return vault ? { ok: true } : { ok: true, response };
|
|
2127
2663
|
}
|
|
2128
2664
|
const sidecar = this.deps.sidecar();
|
|
2129
2665
|
if (!sidecar)
|
|
@@ -2133,14 +2669,16 @@ export class TurnRunner {
|
|
|
2133
2669
|
ok = (await sidecar.call('clarify.respond', { session_id: sessionId, clarify_id: str(entry.clarify_id), response })).ok;
|
|
2134
2670
|
}
|
|
2135
2671
|
catch (error) {
|
|
2672
|
+
if (vault)
|
|
2673
|
+
return { ok: false, error: 'The Agent sidecar did not accept the secure answer; retry in a moment.' };
|
|
2136
2674
|
return { ok: false, error: `The Agent sidecar did not accept the answer (${str(error.message)}); retry in a moment.` };
|
|
2137
2675
|
}
|
|
2138
2676
|
if (!ok)
|
|
2139
2677
|
return { ok: false, stale: true };
|
|
2140
2678
|
const { head } = this.deps.pending.resolveClarify(sessionId, str(entry.clarify_id));
|
|
2141
2679
|
if (head)
|
|
2142
|
-
this.emitToSession(sessionId, 'clarify', head);
|
|
2143
|
-
return { ok: true, response };
|
|
2680
|
+
(await this.emitToSession(sessionId, 'clarify', head));
|
|
2681
|
+
return vault ? { ok: true } : { ok: true, response };
|
|
2144
2682
|
}
|
|
2145
2683
|
}
|
|
2146
2684
|
/** The turn's answer: the last non-error assistant reply with content. */
|