@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
|
@@ -2,18 +2,26 @@
|
|
|
2
2
|
* Read-only projection of the Agent's `state.db` (Python `api/agent_sessions.py`):
|
|
3
3
|
* importable agent session rows with compression-chain collapse, CLI visibility
|
|
4
4
|
* rules, the O(1) change fingerprint used by the gateway watcher, and existence
|
|
5
|
-
* probes. Uses `node:sqlite
|
|
6
|
-
*
|
|
5
|
+
* probes. Uses cached read-only `node:sqlite` handles with a bounded busy timeout
|
|
6
|
+
* and a fresh snapshot per read; no path here ever writes to the database.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, openSync, readSync, closeSync, statSync } from 'node:fs';
|
|
9
9
|
import { DatabaseSync } from 'node:sqlite';
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
10
11
|
import { Worker } from 'node:worker_threads';
|
|
12
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
13
|
+
import { startupTimingScope } from '../startup-timing.js';
|
|
11
14
|
import { str } from '../util.js';
|
|
15
|
+
import { stateDbMessageFingerprint, stateDbMessageKey } from './state-db-identity.js';
|
|
12
16
|
import { MESSAGING_SOURCES } from './source-kind.js';
|
|
13
17
|
import { looksLikeDefaultCliTitle, normalizeSourceName } from './titles.js';
|
|
14
18
|
export const CLI_MIN_UNTITLED_USER_MESSAGE_COUNT = 2;
|
|
15
|
-
|
|
19
|
+
/** The Agent's `_READ_BUSY_TIMEOUT_S` (TAL-890): a DELETE-journal commit by another process blocks readers across its fsyncs. */
|
|
20
|
+
const STATE_DB_BUSY_TIMEOUT_MS = 5000;
|
|
21
|
+
/** The gateway watcher's change check stays cheap; a lock there fails over to a full projection. */
|
|
22
|
+
const STATE_DB_FINGERPRINT_BUSY_TIMEOUT_MS = 500;
|
|
16
23
|
const SOURCE_LABELS = {
|
|
24
|
+
bluebubbles: 'BlueBubbles', dingtalk: 'DingTalk', feishu: 'Feishu', qqbot: 'QQBot', yuanbao: 'Yuanbao', whatsapp_cloud: 'WhatsApp Cloud',
|
|
17
25
|
acp: 'ACP', api_server: 'API', cli: 'CLI', cron: 'Cron', discord: 'Discord', email: 'Email', kanban: 'Kanban', wecom: 'WeCom', wecom_callback: 'WeCom Callback',
|
|
18
26
|
slack: 'Slack', telegram: 'Telegram', tool: 'Tool', tui: 'TUI', webhook: 'Webhook', webui: 'WebUI', weixin: 'Weixin', matrix: 'Matrix', signal: 'Signal',
|
|
19
27
|
};
|
|
@@ -90,6 +98,8 @@ export function isCliSessionRow(row) {
|
|
|
90
98
|
export function isCliSessionRowVisible(row) {
|
|
91
99
|
if (!isCliSessionRow(row))
|
|
92
100
|
return true;
|
|
101
|
+
if (row._lineage_root_id && row._lineage_tip_id)
|
|
102
|
+
return true;
|
|
93
103
|
const actual = positiveInt(row.actual_message_count);
|
|
94
104
|
const messageCount = actual || positiveInt(row.message_count);
|
|
95
105
|
if (messageCount <= 0)
|
|
@@ -108,9 +118,15 @@ export function isCliSessionRowVisible(row) {
|
|
|
108
118
|
return countUserTurns(row) >= CLI_MIN_UNTITLED_USER_MESSAGE_COUNT;
|
|
109
119
|
}
|
|
110
120
|
// ── compression-chain projection ─────────────────────────────────────────
|
|
121
|
+
/** The Agent's `_CHAIN_CAP`: the most hops a walk along a compression lineage takes. Its visited check stops a cycle sooner. */
|
|
122
|
+
const COMPRESSION_CHAIN_CAP = 1000;
|
|
123
|
+
/** The most ids the Agent's `_resume_lineage_ids` returns, a tighter defensive bound than `_CHAIN_CAP`. */
|
|
124
|
+
const RESUME_LINEAGE_CAP = 100;
|
|
111
125
|
function isContinuationSession(parent, child) {
|
|
112
126
|
if (!parent || !child)
|
|
113
127
|
return false;
|
|
128
|
+
if (lower(child.source) === 'tool' || explicitForkMarkers(child, true).length > 0)
|
|
129
|
+
return false;
|
|
114
130
|
if (lower(child.session_source) === 'fork')
|
|
115
131
|
return false;
|
|
116
132
|
const parentSource = lower(parent.source);
|
|
@@ -126,6 +142,37 @@ function isContinuationSession(parent, child) {
|
|
|
126
142
|
const ended = Number.parseFloat(str(endedAt));
|
|
127
143
|
return Number.isFinite(started) && Number.isFinite(ended) && started + 1.0 >= ended;
|
|
128
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* `ids` plus every session a compression continuation (`isContinuationSession`) joins to one of them, up and down, at
|
|
147
|
+
* most `COMPRESSION_CHAIN_CAP` in all: the rows `projectAgentSessionRows` needs to collapse their chains as the sidebar
|
|
148
|
+
* listing does. Forks and other children are never followed, so they cannot crowd a continuation out of the cap.
|
|
149
|
+
*/
|
|
150
|
+
function compressionChainIds(db, cols, ids) {
|
|
151
|
+
if (!['parent_session_id', 'end_reason'].every((c) => cols.has(c)))
|
|
152
|
+
return ids;
|
|
153
|
+
const fields = ['id', 'source', 'session_source', 'model_config', 'started_at', 'parent_session_id', 'end_reason', 'ended_at'].map((c) => cols.has(c) ? c : `NULL AS ${c}`).join(', ');
|
|
154
|
+
const row = db.prepare(`SELECT ${fields} FROM sessions WHERE id = ?`);
|
|
155
|
+
const children = db.prepare(`SELECT ${fields} FROM sessions WHERE parent_session_id = ?`);
|
|
156
|
+
const seen = new Set(ids);
|
|
157
|
+
const pending = [...ids];
|
|
158
|
+
const visit = (next) => { if (next && !seen.has(next) && seen.size < COMPRESSION_CHAIN_CAP) {
|
|
159
|
+
seen.add(next);
|
|
160
|
+
pending.push(next);
|
|
161
|
+
} };
|
|
162
|
+
while (pending.length) {
|
|
163
|
+
const current = row.get(pending.pop());
|
|
164
|
+
if (!current)
|
|
165
|
+
continue;
|
|
166
|
+
const parentId = str(current.parent_session_id);
|
|
167
|
+
if (parentId && isContinuationSession(row.get(parentId), current))
|
|
168
|
+
visit(parentId);
|
|
169
|
+
if (['compression', 'cli_close'].includes(str(current.end_reason)))
|
|
170
|
+
for (const child of children.all(str(current.id)))
|
|
171
|
+
if (isContinuationSession(current, child))
|
|
172
|
+
visit(str(child.id));
|
|
173
|
+
}
|
|
174
|
+
return [...seen];
|
|
175
|
+
}
|
|
129
176
|
function continuationRootId(rowsById, sessionId) {
|
|
130
177
|
if (!sessionId)
|
|
131
178
|
return null;
|
|
@@ -147,7 +194,7 @@ function continuationRootId(rowsById, sessionId) {
|
|
|
147
194
|
return rootId;
|
|
148
195
|
}
|
|
149
196
|
/** Python `_project_agent_session_rows`: collapse compression chains into one sidebar row pointing at the freshest importable tip. */
|
|
150
|
-
export function projectAgentSessionRows(rows) {
|
|
197
|
+
export function projectAgentSessionRows(rows, keepEmpty = false) {
|
|
151
198
|
const rowsById = new Map(rows.map((r) => [str(r.id), r]));
|
|
152
199
|
const childrenByParent = new Map();
|
|
153
200
|
const continuationChildIds = new Set();
|
|
@@ -203,14 +250,22 @@ export function projectAgentSessionRows(rows) {
|
|
|
203
250
|
};
|
|
204
251
|
const projected = [];
|
|
205
252
|
for (const row of rows) {
|
|
253
|
+
if (row._lineage_root_id && row._lineage_tip_id) {
|
|
254
|
+
projected.push({ ...row });
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
206
257
|
if (continuationChildIds.has(str(row.id)))
|
|
207
258
|
continue;
|
|
208
259
|
let segments = 1;
|
|
209
260
|
let tip = row;
|
|
210
261
|
if (['compression', 'cli_close'].includes(str(row.end_reason)))
|
|
211
262
|
[tip, segments] = compressionTip(row);
|
|
212
|
-
|
|
263
|
+
// `keepEmpty` (a direct lookup of receipt-proven Web diagnostic chats, TAL-1040) keeps a turnless plain row.
|
|
264
|
+
if (!tip || positiveInt(tip.actual_message_count) <= 0) {
|
|
265
|
+
if (keepEmpty && tip === row)
|
|
266
|
+
projected.push({ ...row });
|
|
213
267
|
continue;
|
|
268
|
+
}
|
|
214
269
|
if (tip === row) {
|
|
215
270
|
projected.push({ ...row });
|
|
216
271
|
continue;
|
|
@@ -239,22 +294,266 @@ export function projectAgentSessionRows(rows) {
|
|
|
239
294
|
projected.sort((a, b) => score(b.last_activity, b.started_at) - score(a.last_activity, a.started_at));
|
|
240
295
|
return projected;
|
|
241
296
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
297
|
+
/** Bound descriptors and page caches even when deleted profiles are never read again. */
|
|
298
|
+
const STATE_DB_HANDLE_CACHE_MAX = 16;
|
|
299
|
+
const readonlyStateDbs = new Map();
|
|
300
|
+
const schemaCaches = new WeakMap();
|
|
301
|
+
/** Cache ownership ends at shutdown; worker termination also disposes its native SQLite resources. */
|
|
302
|
+
function closeStateDbs(paths = readonlyStateDbs.keys()) {
|
|
303
|
+
for (const path of paths) {
|
|
304
|
+
const cached = readonlyStateDbs.get(path);
|
|
305
|
+
if (cached?.db.isOpen)
|
|
306
|
+
cached.db.close();
|
|
307
|
+
readonlyStateDbs.delete(path);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
process.once('exit', () => { closeStateDbs(); });
|
|
311
|
+
/** One handle per file identity. The caller ends the read snapshot in finally, leaving closure to the cache. */
|
|
312
|
+
function openStateDbReadonly(dbPath, busyTimeoutMs = STATE_DB_BUSY_TIMEOUT_MS) {
|
|
313
|
+
const { ino, dev, mtimeNs } = statSync(dbPath, { bigint: true });
|
|
314
|
+
let cached = readonlyStateDbs.get(dbPath);
|
|
315
|
+
// Raw writes need not advance SQLite's change counter; revalidate the main file after any such change.
|
|
316
|
+
if (cached && (cached.ino !== ino || cached.dev !== dev || cached.mtimeNs !== mtimeNs || !cached.db.isOpen)) {
|
|
317
|
+
closeStateDbs([dbPath]);
|
|
318
|
+
cached = undefined;
|
|
319
|
+
}
|
|
320
|
+
if (!cached) {
|
|
321
|
+
if (readonlyStateDbs.size >= STATE_DB_HANDLE_CACHE_MAX)
|
|
322
|
+
closeStateDbs([readonlyStateDbs.keys().next().value]);
|
|
323
|
+
const db = new DatabaseSync(dbPath, { readOnly: true, timeout: busyTimeoutMs });
|
|
324
|
+
cached = { db, ino, dev, mtimeNs, schemaVersion: null, columns: new Map(), indexes: new Map() };
|
|
325
|
+
schemaCaches.set(db, cached);
|
|
326
|
+
}
|
|
327
|
+
readonlyStateDbs.delete(dbPath);
|
|
328
|
+
readonlyStateDbs.set(dbPath, cached);
|
|
329
|
+
const { db } = cached;
|
|
330
|
+
try {
|
|
331
|
+
db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.trunc(busyTimeoutMs))}`);
|
|
332
|
+
// The schema cookie and all probes/queries must describe the same committed snapshot.
|
|
333
|
+
db.exec('BEGIN');
|
|
334
|
+
const version = db.prepare('PRAGMA schema_version').get().schema_version;
|
|
335
|
+
if (cached.schemaVersion !== version) {
|
|
336
|
+
cached.columns.clear();
|
|
337
|
+
cached.indexes.clear();
|
|
338
|
+
cached.schemaVersion = version;
|
|
339
|
+
}
|
|
340
|
+
return db;
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
closeStateDbs([dbPath]);
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
249
346
|
}
|
|
347
|
+
function finishStateDbRead(db) {
|
|
348
|
+
try {
|
|
349
|
+
db.exec('COMMIT');
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// A failed read can poison COMMIT too; discard the handle without hiding the reader's failure result.
|
|
353
|
+
db.close();
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/** A failed probe throws, so unreadable storage is never mistaken for an older schema (TAL-785). */
|
|
250
357
|
function tableColumns(db, table) {
|
|
358
|
+
const cache = schemaCaches.get(db).columns;
|
|
359
|
+
let columns = cache.get(table);
|
|
360
|
+
if (!columns) {
|
|
361
|
+
columns = new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name));
|
|
362
|
+
cache.set(table, columns);
|
|
363
|
+
}
|
|
364
|
+
return columns;
|
|
365
|
+
}
|
|
366
|
+
function tableIndexes(db, table) {
|
|
367
|
+
const cache = schemaCaches.get(db).indexes;
|
|
368
|
+
const indexes = cache.get(table);
|
|
369
|
+
if (indexes)
|
|
370
|
+
return indexes;
|
|
251
371
|
try {
|
|
252
|
-
|
|
372
|
+
const found = new Set(db.prepare(`PRAGMA index_list(${table})`).all().map((r) => r.name));
|
|
373
|
+
cache.set(table, found);
|
|
374
|
+
return found;
|
|
253
375
|
}
|
|
254
376
|
catch {
|
|
255
377
|
return new Set();
|
|
256
378
|
}
|
|
257
379
|
}
|
|
380
|
+
/** Agent `_sql_freshest_of`: the `last_activity_at` heartbeat can lag a newer message, so it never ranks alone. */
|
|
381
|
+
function freshestActivity(lastActivityAt, lastMessage) {
|
|
382
|
+
return `MAX(COALESCE(${lastActivityAt}, 0), COALESCE(${lastMessage}, 0), COALESCE(s.started_at, 0))`;
|
|
383
|
+
}
|
|
384
|
+
/** Stale Agent statistics can favor a non-covering index and read every message row. */
|
|
385
|
+
function lastMessageTimestamp(indexed, alias = 'm') {
|
|
386
|
+
return `(SELECT MAX(${alias}.timestamp) FROM messages ${alias}${indexed ? ' INDEXED BY idx_messages_session' : ''} WHERE ${alias}.session_id = s.id)`;
|
|
387
|
+
}
|
|
388
|
+
const LOCAL_POLICY_PREFIX = 'gateway.local_policy.v1:';
|
|
389
|
+
/** Read-only equivalent of Agent validate_local_lineage. A receipt, never a reset edge alone, proves ownership. */
|
|
390
|
+
function localLineages(db, sid) {
|
|
391
|
+
const result = new Map();
|
|
392
|
+
if (!tableColumns(db, 'state_meta').has('value'))
|
|
393
|
+
return result;
|
|
394
|
+
const cols = tableColumns(db, 'sessions');
|
|
395
|
+
if (!['chat_id', 'user_id', 'session_key', 'model_config', 'parent_session_id', 'end_reason'].every((c) => cols.has(c)))
|
|
396
|
+
return result;
|
|
397
|
+
const meta = db.prepare('SELECT value FROM state_meta WHERE key = ?');
|
|
398
|
+
const fetch = db.prepare('SELECT * FROM sessions WHERE id IN (SELECT value FROM json_each(?))');
|
|
399
|
+
const messageCols = tableColumns(db, 'messages');
|
|
400
|
+
const lastMessage = ['session_id', 'timestamp'].every((c) => messageCols.has(c)) ? lastMessageTimestamp(tableIndexes(db, 'messages').has('idx_messages_session')) : 'NULL';
|
|
401
|
+
const activity = freshestActivity(cols.has('last_activity_at') ? 's.last_activity_at' : 'NULL', lastMessage);
|
|
402
|
+
// Agent _CHAIN_STEP_SQL prefers a compression child, then a live child, then recency and id.
|
|
403
|
+
const children = db.prepare(`SELECT s.* FROM sessions s WHERE s.parent_session_id = ? ORDER BY CASE WHEN s.end_reason = 'compression' THEN 0 WHEN s.ended_at IS NULL THEN 1 ELSE 2 END, ${activity} DESC, s.started_at DESC, s.id DESC`);
|
|
404
|
+
const explicitChild = (child, includeReset) => {
|
|
405
|
+
return lower(child.source) === 'tool' || lower(child.session_source) === 'fork'
|
|
406
|
+
|| explicitForkMarkers(child, includeReset).length > 0;
|
|
407
|
+
};
|
|
408
|
+
const compressionChildren = (parent) => str(parent.end_reason) === 'compression'
|
|
409
|
+
? children.all(str(parent.id)).filter((child) => !explicitChild(child, true)) : [];
|
|
410
|
+
const owner = sid ? db.prepare('SELECT chat_id FROM sessions WHERE id = ?').get(sid)?.chat_id || sid : null;
|
|
411
|
+
const receipts = owner
|
|
412
|
+
? db.prepare('SELECT key, value FROM state_meta WHERE key = ?').all(`${LOCAL_POLICY_PREFIX}${str(owner)}`)
|
|
413
|
+
: db.prepare('SELECT key, value FROM state_meta WHERE key GLOB ?').iterate(`${LOCAL_POLICY_PREFIX}*`);
|
|
414
|
+
for (const saved of receipts) {
|
|
415
|
+
try {
|
|
416
|
+
if (saved.value.length > 1_000_000)
|
|
417
|
+
continue;
|
|
418
|
+
const receipt = JSON.parse(saved.value);
|
|
419
|
+
const owner = saved.key.slice(LOCAL_POLICY_PREFIX.length);
|
|
420
|
+
const ids = receipt.lineage;
|
|
421
|
+
const entry = receipt.entry;
|
|
422
|
+
if (receipt.session_id !== owner || !Array.isArray(ids) || ids.length < 2 || ids.length > COMPRESSION_CHAIN_CAP
|
|
423
|
+
|| !ids.every((id) => typeof id === 'string' && Boolean(id)) || new Set(ids).size !== ids.length
|
|
424
|
+
|| ids[0] !== owner || ids.at(-1) !== entry?.session_id)
|
|
425
|
+
continue;
|
|
426
|
+
if (!['profile_id', 'principal_id', 'request_id', 'route'].every((key) => typeof receipt[key] === 'string' && receipt[key]))
|
|
427
|
+
continue;
|
|
428
|
+
if ('legacy_session_id' in receipt) {
|
|
429
|
+
const legacy = meta.get(`gateway.local_legacy.v1:${owner}`);
|
|
430
|
+
if (receipt.legacy_session_id !== owner || !legacy)
|
|
431
|
+
continue;
|
|
432
|
+
const binding = JSON.parse(legacy.value);
|
|
433
|
+
if (!['session_id', 'profile_id', 'principal_id', 'legacy_session_id'].every((key) => binding[key] === receipt[key]))
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// Python json.dumps uses ensure_ascii=True, including UTF-16 surrogate pairs.
|
|
438
|
+
const identity = JSON.stringify([receipt.profile_id, receipt.principal_id, receipt.request_id]).replace(/[\u007f-\uffff]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
439
|
+
if (owner !== `local-${createHash('sha256').update(identity).digest('hex')}`)
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const byId = new Map(fetch.all(JSON.stringify(ids)).map((row) => [str(row.id), row]));
|
|
443
|
+
const rows = ids.map((id) => byId.get(id));
|
|
444
|
+
if (!rows.every((row) => Boolean(row) && row.chat_id === owner && row.user_id === receipt.principal_id && row.session_key === receipt.route))
|
|
445
|
+
continue;
|
|
446
|
+
let contextStart = 0;
|
|
447
|
+
let valid = true;
|
|
448
|
+
for (let i = 1; i < rows.length; i += 1) {
|
|
449
|
+
const parent = rows[i - 1], child = rows[i];
|
|
450
|
+
if (child.parent_session_id !== parent.id || explicitChild(child, false)) {
|
|
451
|
+
valid = false;
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
if (parent.end_reason === 'session_reset') {
|
|
455
|
+
const config = jsonLoadsIfString(child.model_config);
|
|
456
|
+
if (!config || config._reset_from !== parent.id) {
|
|
457
|
+
valid = false;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
contextStart = i;
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
const next = compressionChildren(parent);
|
|
464
|
+
if (next[0]?.id !== child.id) {
|
|
465
|
+
valid = false;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (!valid || compressionChildren(rows.at(-1)).length)
|
|
471
|
+
continue;
|
|
472
|
+
const lineage = { owner, ids, context: ids.slice(contextStart), rows, title: [...rows].reverse().find((row) => row.title)?.title ?? null };
|
|
473
|
+
for (const id of ids)
|
|
474
|
+
result.set(id, lineage);
|
|
475
|
+
}
|
|
476
|
+
catch { /* Malformed receipts cannot attach history to another conversation. */ }
|
|
477
|
+
}
|
|
478
|
+
return result;
|
|
479
|
+
}
|
|
480
|
+
/** The validated receipt's current physical target, or the original id on old Agents / unproven metadata. */
|
|
481
|
+
export function stateDbLocalTarget(dbPath, sid, opts = {}) {
|
|
482
|
+
if (!existsSync(dbPath)) {
|
|
483
|
+
opts.onRead?.(sid);
|
|
484
|
+
return sid;
|
|
485
|
+
}
|
|
486
|
+
let db;
|
|
487
|
+
try {
|
|
488
|
+
db = openStateDbReadonly(dbPath);
|
|
489
|
+
}
|
|
490
|
+
catch {
|
|
491
|
+
return sid;
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const target = localLineages(db, sid).get(sid)?.ids.at(-1) ?? sid;
|
|
495
|
+
opts.onRead?.(target);
|
|
496
|
+
return target;
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
return sid;
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
finishStateDbRead(db);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/** `stateDbLocalTarget` plus whether state.db answered (a missing one does), so only a real answer seeds a routing cache. */
|
|
506
|
+
function stateDbLocalTargetRead(dbPath, sid) {
|
|
507
|
+
let read = false;
|
|
508
|
+
const target = stateDbLocalTarget(dbPath, sid, { onRead: () => { read = true; } });
|
|
509
|
+
return { target, read };
|
|
510
|
+
}
|
|
511
|
+
export const AGENT_STORAGE_OK = Object.freeze({ state: 'ok', reason: null });
|
|
512
|
+
/**
|
|
513
|
+
* The corrupt-storage reason when `error` is SQLite's `SQLITE_CORRUPT` or `SQLITE_NOTADB` (the Agent's
|
|
514
|
+
* `hermes_state_health`), else null. The message is matched too, since a listing worker failure crosses the thread
|
|
515
|
+
* boundary as its message alone.
|
|
516
|
+
*/
|
|
517
|
+
export function stateDbCorruptReason(error) {
|
|
518
|
+
const code = error?.errcode;
|
|
519
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
520
|
+
const corrupt = (typeof code === 'number' && [11, 26].includes(code & 0xff)) || /malformed|not a database/i.test(message);
|
|
521
|
+
return corrupt ? `Agent session storage (state.db) is damaged: ${message}. Agent sessions are unavailable until it is repaired or restored from a backup.` : null;
|
|
522
|
+
}
|
|
523
|
+
/** The storage state a failed `state.db` read reports: `corrupt` when SQLite says the file is damaged, else `unavailable`. */
|
|
524
|
+
export function stateDbReadFailure(error) {
|
|
525
|
+
const corrupt = stateDbCorruptReason(error);
|
|
526
|
+
if (corrupt)
|
|
527
|
+
return { state: 'corrupt', reason: corrupt };
|
|
528
|
+
return { state: 'unavailable', reason: `Agent session storage (state.db) could not be read: ${error instanceof Error ? error.message : String(error)}.` };
|
|
529
|
+
}
|
|
530
|
+
function outdatedStateDbStorage() {
|
|
531
|
+
return { state: 'outdated', reason: "state.db has no 'source' column (older hermes-agent?). Agent sessions unavailable. Upgrade hermes-agent to fix this." };
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* TAL-785: whether `state.db` can serve the session list: `corrupt` when SQLite reports the file damaged, `unavailable`
|
|
535
|
+
* when it cannot be read for another reason (permissions, a lock, an I/O error), `outdated` when a readable database
|
|
536
|
+
* has no `sessions.source` column. A missing file is `ok`. Full-text index damage is never probed, so it stays `ok`, as
|
|
537
|
+
* the Agent treats it.
|
|
538
|
+
*/
|
|
539
|
+
export function stateDbStorage(dbPath) {
|
|
540
|
+
if (!existsSync(dbPath))
|
|
541
|
+
return AGENT_STORAGE_OK;
|
|
542
|
+
try {
|
|
543
|
+
const db = openStateDbReadonly(dbPath);
|
|
544
|
+
try {
|
|
545
|
+
if (!tableColumns(db, 'sessions').has('source'))
|
|
546
|
+
return outdatedStateDbStorage();
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
finishStateDbRead(db);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
return stateDbReadFailure(error);
|
|
554
|
+
}
|
|
555
|
+
return AGENT_STORAGE_OK;
|
|
556
|
+
}
|
|
258
557
|
const warnedNoSourceColumn = new Set();
|
|
259
558
|
let readDelayMs = 0;
|
|
260
559
|
/** Test hook: each listing read (and each listing worker request) blocks its thread this long first, standing in for a slow `state.db` (TAL-920). */
|
|
@@ -268,11 +567,15 @@ export function setStateDbReadDelayForTests(ms) { readDelayMs = ms; }
|
|
|
268
567
|
* `idx_messages_session` exactly as Python does when its heal fails.
|
|
269
568
|
*/
|
|
270
569
|
export function readImportableAgentSessionRows(dbPath, opts = {}) {
|
|
570
|
+
const result = readImportableAgentSessionRowBatches(dbPath, [opts]).rows[0];
|
|
571
|
+
if (result instanceof Error)
|
|
572
|
+
throw result;
|
|
573
|
+
return result;
|
|
574
|
+
}
|
|
575
|
+
/** All listing windows share one read snapshot, schema, validated local lineages and ownership lookup. */
|
|
576
|
+
export function readImportableAgentSessionRowBatches(dbPath, queries) {
|
|
271
577
|
if (!existsSync(dbPath))
|
|
272
|
-
return [];
|
|
273
|
-
const limit = opts.limit === undefined ? 200 : opts.limit;
|
|
274
|
-
const exclude = opts.excludeSources === undefined ? ['cron', 'webui'] : opts.excludeSources;
|
|
275
|
-
const include = opts.includeSources ?? null;
|
|
578
|
+
return { rows: queries.map(() => []), storage: AGENT_STORAGE_OK };
|
|
276
579
|
if (readDelayMs > 0)
|
|
277
580
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, readDelayMs);
|
|
278
581
|
const db = openStateDbReadonly(dbPath);
|
|
@@ -282,29 +585,24 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
|
|
|
282
585
|
if (!sessionCols.has('source')) {
|
|
283
586
|
if (!warnedNoSourceColumn.has(dbPath)) {
|
|
284
587
|
warnedNoSourceColumn.add(dbPath);
|
|
285
|
-
|
|
588
|
+
queries[0]?.log?.(`[webui] agent session listing skipped: state.db at ${dbPath} has no 'source' column (older hermes-agent?). Agent sessions unavailable. Upgrade hermes-agent to fix this.`);
|
|
286
589
|
}
|
|
287
|
-
return [];
|
|
590
|
+
return { rows: queries.map(() => []), storage: outdatedStateDbStorage() };
|
|
288
591
|
}
|
|
592
|
+
const only = queries.length === 1 ? queries[0]?.sessionIds : undefined;
|
|
593
|
+
const local = localLineages(db, only?.length === 1 ? only[0] : undefined);
|
|
289
594
|
const col = (name) => (sessionCols.has(name) ? `s.${name}` : `NULL AS ${name}`);
|
|
290
595
|
const messagesHasSessionId = messageCols.has('session_id');
|
|
291
596
|
const messagesHasTimestamp = messageCols.has('timestamp');
|
|
292
597
|
const countCol = messageCols.has('id') ? 'id' : 'session_id';
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
messagesIndexPresent = indexes.has('idx_messages_session');
|
|
299
|
-
userMessagesIndexPresent = indexes.has('idx_messages_session_user');
|
|
300
|
-
}
|
|
301
|
-
catch {
|
|
302
|
-
messagesIndexPresent = false;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
598
|
+
const messageIndexes = messagesHasSessionId && messagesHasTimestamp ? tableIndexes(db, 'messages') : new Set();
|
|
599
|
+
const sessionIndexes = tableIndexes(db, 'sessions');
|
|
600
|
+
const hasPolicies = Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'state_meta'").get());
|
|
601
|
+
const messagesIndexPresent = messageIndexes.has('idx_messages_session');
|
|
602
|
+
const userMessagesIndexPresent = messageIndexes.has('idx_messages_session_user');
|
|
305
603
|
let actualCountExpr;
|
|
306
604
|
let userCountExpr;
|
|
307
|
-
let
|
|
605
|
+
let baseLastActivityExpr;
|
|
308
606
|
let joinClause = '';
|
|
309
607
|
let groupByClause = '';
|
|
310
608
|
if (messagesHasSessionId) {
|
|
@@ -317,12 +615,12 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
|
|
|
317
615
|
}
|
|
318
616
|
else
|
|
319
617
|
userCountExpr = actualCountExpr;
|
|
320
|
-
|
|
618
|
+
baseLastActivityExpr = messagesHasTimestamp ? lastMessageTimestamp(messagesIndexPresent, 'lm') : 'NULL';
|
|
321
619
|
}
|
|
322
620
|
else {
|
|
323
621
|
actualCountExpr = `COUNT(m.${countCol})`;
|
|
324
622
|
userCountExpr = messageCols.has('role') ? "EXISTS(SELECT 1 FROM messages um WHERE um.session_id = s.id AND LOWER(um.role) = 'user')" : `COUNT(m.${countCol})`;
|
|
325
|
-
|
|
623
|
+
baseLastActivityExpr = messagesHasTimestamp ? 'MAX(m.timestamp)' : 'NULL';
|
|
326
624
|
joinClause = 'LEFT JOIN messages m ON m.session_id = s.id';
|
|
327
625
|
groupByClause = 'GROUP BY s.id';
|
|
328
626
|
}
|
|
@@ -330,75 +628,151 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
|
|
|
330
628
|
else {
|
|
331
629
|
actualCountExpr = 's.message_count';
|
|
332
630
|
userCountExpr = 's.message_count';
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
let orderBy = 'ORDER BY s.started_at DESC';
|
|
336
|
-
let latestMessagesCte = null;
|
|
337
|
-
let candidateOrder = 'ORDER BY s.started_at DESC';
|
|
338
|
-
const where = ['s.source IS NOT NULL'];
|
|
339
|
-
const params = [];
|
|
340
|
-
const included = (include ?? []).map(String).filter(Boolean);
|
|
341
|
-
if (included.length) {
|
|
342
|
-
where.push(`s.source IN (${included.map(() => '?').join(', ')})`);
|
|
343
|
-
params.push(...included);
|
|
631
|
+
baseLastActivityExpr = 'NULL';
|
|
344
632
|
}
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
633
|
+
const candidates = (opts) => {
|
|
634
|
+
const limit = opts.limit === undefined ? 200 : opts.limit;
|
|
635
|
+
const exclude = opts.excludeSources === undefined ? ['cron', 'webui'] : opts.excludeSources;
|
|
636
|
+
const include = opts.includeSources ?? null;
|
|
637
|
+
let lastActivityExpr = baseLastActivityExpr;
|
|
638
|
+
let orderBy = 'ORDER BY s.started_at DESC';
|
|
639
|
+
let latestMessagesCte = null;
|
|
640
|
+
let candidateOrder = 'ORDER BY s.started_at DESC';
|
|
641
|
+
const where = ['s.source IS NOT NULL'];
|
|
642
|
+
const params = [];
|
|
643
|
+
if (opts.sessionIds !== undefined) {
|
|
644
|
+
where.push('s.id IN (SELECT value FROM json_each(?))');
|
|
645
|
+
const ids = opts.sessionIds.map((id) => local.get(id)?.ids.at(-1) ?? id);
|
|
646
|
+
params.push(JSON.stringify(opts.compressionChains ? compressionChainIds(db, sessionCols, ids) : ids));
|
|
647
|
+
}
|
|
648
|
+
const superseded = [...local].filter(([id, lineage]) => id !== lineage.ids.at(-1)).map(([id]) => id);
|
|
649
|
+
if (superseded.length) {
|
|
650
|
+
where.push('s.id NOT IN (SELECT value FROM json_each(?))');
|
|
651
|
+
params.push(JSON.stringify(superseded));
|
|
652
|
+
}
|
|
653
|
+
const policySource = (id) => `(SELECT CASE WHEN json_valid(value) THEN json_extract(value, '$.policy.source') END FROM state_meta WHERE key = '${LOCAL_POLICY_PREFIX}' || ${id})`;
|
|
654
|
+
const sourceSql = hasPolicies ? `COALESCE(${policySource('s.id')}, ${sessionCols.has('chat_id') ? policySource('s.chat_id') : 'NULL'}, s.source)` : 's.source';
|
|
655
|
+
const included = (include ?? []).map(String).filter(Boolean);
|
|
656
|
+
if (included.length) {
|
|
657
|
+
where.push(`${sourceSql} IN (${included.map(() => '?').join(', ')})`);
|
|
658
|
+
params.push(...included);
|
|
659
|
+
}
|
|
660
|
+
const excluded = (exclude ?? []).map(String).filter(Boolean);
|
|
661
|
+
if (excluded.length) {
|
|
662
|
+
where.push(`${sourceSql} NOT IN (${excluded.map(() => '?').join(', ')})`);
|
|
663
|
+
params.push(...excluded);
|
|
664
|
+
}
|
|
665
|
+
const preaggregated = messagesHasSessionId && messagesHasTimestamp && included.length === 1 && included[0] === 'cron' && !messagesIndexPresent;
|
|
666
|
+
// TAL-1163: the Agent's activity index preselects the window; exact recency is computed for those candidates only.
|
|
667
|
+
const activityIndexed = limit !== null && messagesHasSessionId && messagesHasTimestamp && sessionCols.has('last_activity_at')
|
|
668
|
+
&& sessionIndexes.has('idx_sessions_effective_activity');
|
|
669
|
+
if (activityIndexed) {
|
|
670
|
+
lastActivityExpr = freshestActivity('s.last_activity_at', lastActivityExpr);
|
|
671
|
+
orderBy = 'ORDER BY last_activity DESC';
|
|
672
|
+
candidateOrder = 'ORDER BY COALESCE(s.last_activity_at, s.started_at) DESC, s.started_at DESC, s.id DESC';
|
|
673
|
+
}
|
|
674
|
+
else if (preaggregated) {
|
|
675
|
+
orderBy = 'ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC';
|
|
676
|
+
latestMessagesCte = 'latest_messages AS (SELECT mx.session_id AS session_id, MAX(mx.timestamp) AS last_message_at FROM messages mx GROUP BY mx.session_id)';
|
|
677
|
+
candidateOrder = 'ORDER BY COALESCE(lm.last_message_at, s.started_at) DESC, s.started_at DESC';
|
|
678
|
+
}
|
|
679
|
+
else if (messagesHasSessionId && messagesHasTimestamp) {
|
|
680
|
+
orderBy = messagesIndexPresent ? 'ORDER BY COALESCE(last_activity, s.started_at) DESC' : 'ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC';
|
|
681
|
+
candidateOrder = `ORDER BY COALESCE(${lastMessageTimestamp(messagesIndexPresent, 'mx')}, s.started_at) DESC, s.started_at DESC`;
|
|
682
|
+
}
|
|
683
|
+
const selectSql = `SELECT s.id, s.title, s.model, s.message_count, s.started_at, s.source, ${['model_config', 'session_source', 'user_id', 'chat_id', 'chat_type', 'thread_id', 'session_key', 'origin_chat_id', 'origin_user_id', 'platform', 'parent_session_id', 'ended_at', 'end_reason'].map(col).join(', ')}, ${actualCountExpr} AS actual_message_count, ${userCountExpr} AS actual_user_message_count, ${lastActivityExpr} AS last_activity`;
|
|
684
|
+
let raw;
|
|
685
|
+
if (limit !== null) {
|
|
686
|
+
const resultLimit = Math.max(0, Math.trunc(limit));
|
|
687
|
+
if (resultLimit === 0)
|
|
688
|
+
return [];
|
|
689
|
+
const candidateLimit = Math.max(resultLimit * 8, resultLimit);
|
|
690
|
+
const candidateCte = latestMessagesCte
|
|
691
|
+
? `WITH ${latestMessagesCte}, candidates AS (SELECT s.id FROM sessions s LEFT JOIN latest_messages lm ON lm.session_id = s.id WHERE ${where.join(' AND ')} ${candidateOrder} LIMIT ?)`
|
|
692
|
+
: `WITH candidates AS (SELECT s.id FROM sessions s WHERE ${where.join(' AND ')} ${candidateOrder} LIMIT ?)`;
|
|
693
|
+
raw = db.prepare(`${candidateCte} ${selectSql} FROM sessions s JOIN candidates c ON c.id = s.id ${joinClause} ${groupByClause} ${orderBy}`).all(...params, candidateLimit);
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
raw = db.prepare(`${selectSql} FROM sessions s ${joinClause} WHERE ${where.join(' AND ')} ${groupByClause} ${orderBy}`).all(...params);
|
|
697
|
+
}
|
|
698
|
+
return raw;
|
|
699
|
+
};
|
|
700
|
+
const batches = queries.map((opts) => {
|
|
701
|
+
try {
|
|
702
|
+
return candidates(opts);
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
return error;
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
const ids = [...new Set(batches.flatMap((rows) => rows instanceof Error ? [] : rows.map((r) => str(r.id))))];
|
|
709
|
+
let owners;
|
|
710
|
+
try {
|
|
711
|
+
owners = sessionSources(db, sessionCols, ids);
|
|
374
712
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return projected;
|
|
380
|
-
const selected = projected.slice(0, Math.max(0, Math.trunc(limit)));
|
|
381
|
-
const have = new Set(selected.map((r) => str(r.id)));
|
|
382
|
-
const byId = new Map(projected.filter((r) => r.id).map((r) => [str(r.id), r]));
|
|
383
|
-
const pending = [...selected];
|
|
384
|
-
while (pending.length) {
|
|
385
|
-
const row = pending.pop();
|
|
386
|
-
if (lower(row.raw_source || row.source) !== 'subagent')
|
|
387
|
-
continue;
|
|
388
|
-
const parentId = str(row.parent_session_id);
|
|
389
|
-
if (!parentId || have.has(parentId))
|
|
390
|
-
continue;
|
|
391
|
-
const parent = byId.get(parentId);
|
|
392
|
-
if (!parent || lower(parent.raw_source || parent.source) !== 'subagent')
|
|
393
|
-
continue;
|
|
394
|
-
selected.push(parent);
|
|
395
|
-
have.add(parentId);
|
|
396
|
-
pending.push(parent);
|
|
713
|
+
catch (error) {
|
|
714
|
+
if (queries.length === 1)
|
|
715
|
+
throw error;
|
|
716
|
+
// Retry ownership per window only on damage, so a background failure cannot hide healthy interactive rows.
|
|
397
717
|
}
|
|
398
|
-
|
|
718
|
+
const rows = batches.map((batch, i) => {
|
|
719
|
+
if (batch instanceof Error)
|
|
720
|
+
return batch;
|
|
721
|
+
let batchOwners = owners;
|
|
722
|
+
if (batchOwners === undefined) {
|
|
723
|
+
try {
|
|
724
|
+
batchOwners = sessionSources(db, sessionCols, batch.map((row) => str(row.id)));
|
|
725
|
+
}
|
|
726
|
+
catch (error) {
|
|
727
|
+
return error;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
let raw = batch;
|
|
731
|
+
const opts = queries[i];
|
|
732
|
+
const limit = opts.limit === undefined ? 200 : opts.limit;
|
|
733
|
+
raw = raw.map((r) => { const owner = batchOwners.get(str(r.id)); return { ...r, source: typeof owner === 'object' ? owner.source : owner || r.source }; });
|
|
734
|
+
const keepEmpty = opts.keepEmpty === true && opts.sessionIds !== undefined;
|
|
735
|
+
let projected = projectAgentSessionRows(raw.map((r) => {
|
|
736
|
+
const lineage = local.get(str(r.id));
|
|
737
|
+
if (!lineage)
|
|
738
|
+
return { ...r };
|
|
739
|
+
const root = lineage.rows[0];
|
|
740
|
+
const title = lineage.title || r.title;
|
|
741
|
+
return { ...r, title, started_at: root.started_at, last_activity: r.last_activity ?? r.started_at, parent_session_id: root.parent_session_id,
|
|
742
|
+
_lineage_root_id: lineage.owner, _lineage_tip_id: r.id, _lineage_ids: [...lineage.ids], _compression_segment_count: lineage.ids.length };
|
|
743
|
+
}), keepEmpty);
|
|
744
|
+
projected = projected.map((r) => {
|
|
745
|
+
const parent = local.get(str(r.parent_session_id));
|
|
746
|
+
return { ...r, ...normalizeAgentSessionSource(r.source), ...(parent ? { _parent_lineage_root_id: parent.owner, parent_title: parent.title, parent_source: parent.rows.at(-1)?.source ?? null } : {}) };
|
|
747
|
+
});
|
|
748
|
+
if (!keepEmpty)
|
|
749
|
+
projected = projected.filter(isCliSessionRowVisible);
|
|
750
|
+
if (limit === null)
|
|
751
|
+
return projected;
|
|
752
|
+
const selected = projected.slice(0, Math.max(0, Math.trunc(limit)));
|
|
753
|
+
const have = new Set(selected.map((r) => str(r.id)));
|
|
754
|
+
const byId = new Map(projected.filter((r) => r.id).map((r) => [str(r.id), r]));
|
|
755
|
+
const pending = [...selected];
|
|
756
|
+
while (pending.length) {
|
|
757
|
+
const row = pending.pop();
|
|
758
|
+
if (lower(row.raw_source || row.source) !== 'subagent')
|
|
759
|
+
continue;
|
|
760
|
+
const parentId = str(row.parent_session_id);
|
|
761
|
+
if (!parentId || have.has(parentId))
|
|
762
|
+
continue;
|
|
763
|
+
const parent = byId.get(parentId);
|
|
764
|
+
if (!parent || lower(parent.raw_source || parent.source) !== 'subagent')
|
|
765
|
+
continue;
|
|
766
|
+
selected.push(parent);
|
|
767
|
+
have.add(parentId);
|
|
768
|
+
pending.push(parent);
|
|
769
|
+
}
|
|
770
|
+
return selected;
|
|
771
|
+
});
|
|
772
|
+
return { rows, storage: AGENT_STORAGE_OK };
|
|
399
773
|
}
|
|
400
774
|
finally {
|
|
401
|
-
db
|
|
775
|
+
finishStateDbRead(db);
|
|
402
776
|
}
|
|
403
777
|
}
|
|
404
778
|
/** Python `agent_session_rows_existing`: batch existence probe; assumes present on any failure so nothing is pruned by mistake. */
|
|
@@ -429,12 +803,61 @@ export function agentSessionRowsExisting(dbPath, sessionIds) {
|
|
|
429
803
|
return wanted;
|
|
430
804
|
}
|
|
431
805
|
finally {
|
|
432
|
-
db
|
|
806
|
+
finishStateDbRead(db);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
/** Scheduler admissions are the identity of gateway-fired cron runs, whose session ids stay local-*. */
|
|
810
|
+
function cronAdmissionJobIds(db, terminalOnly = false) {
|
|
811
|
+
const found = new Map();
|
|
812
|
+
const cols = tableColumns(db, 'session_admissions');
|
|
813
|
+
if (!['request_id', 'target_session_id', 'principal_id'].every((col) => cols.has(col)) || (terminalOnly && !cols.has('status')))
|
|
814
|
+
return found;
|
|
815
|
+
// The principal predicate selects the Agent's partial covering index, excluding interactive admissions.
|
|
816
|
+
const rows = db.prepare(`SELECT target_session_id, request_id FROM session_admissions WHERE principal_id = 'cron-owner' AND request_id LIKE 'cron:%'${terminalOnly ? " AND status = 'terminal'" : ''}`).all();
|
|
817
|
+
for (const row of rows) {
|
|
818
|
+
const match = /^cron:([^:]+):([^:]+)$/.exec(row.request_id);
|
|
819
|
+
if (match)
|
|
820
|
+
found.set(row.target_session_id, match[1]);
|
|
821
|
+
}
|
|
822
|
+
return found;
|
|
823
|
+
}
|
|
824
|
+
/** Read-only admission lookup, built once per session listing; older Agent stores have none. */
|
|
825
|
+
export function stateDbCronJobIds(dbPath) {
|
|
826
|
+
if (!existsSync(dbPath))
|
|
827
|
+
return new Map();
|
|
828
|
+
let db;
|
|
829
|
+
try {
|
|
830
|
+
db = openStateDbReadonly(dbPath);
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
return new Map();
|
|
834
|
+
}
|
|
835
|
+
try {
|
|
836
|
+
return cronAdmissionJobIds(db);
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
return new Map();
|
|
840
|
+
}
|
|
841
|
+
finally {
|
|
842
|
+
finishStateDbRead(db);
|
|
433
843
|
}
|
|
434
844
|
}
|
|
845
|
+
/** Shared job identity rule for completion links and sidebar titles, including legacy job ids with underscores. */
|
|
846
|
+
export function cronSessionJobId(sid, jobIds, admissions) {
|
|
847
|
+
const admitted = admissions.get(sid);
|
|
848
|
+
if (admitted)
|
|
849
|
+
return admitted;
|
|
850
|
+
const legacy = /^cron_(.+)_\d{8}_\d{6}$/.exec(sid);
|
|
851
|
+
if (legacy)
|
|
852
|
+
return legacy[1];
|
|
853
|
+
// Preserve prefix matching for older non-timestamp session ids.
|
|
854
|
+
return [...jobIds].filter((id) => sid.startsWith(`cron_${id}_`)).reduce((best, id) => best === null || id.length > best.length ? id : best, null);
|
|
855
|
+
}
|
|
435
856
|
/**
|
|
436
|
-
* Python `_latest_cron_session_info_for_jobs`: the newest-started cron session for each completed job
|
|
437
|
-
*
|
|
857
|
+
* Python `_latest_cron_session_info_for_jobs`: the newest-started cron session for each completed job, excluding
|
|
858
|
+
* non-terminal gateway admissions so the next run cannot steal the completion link. A session id
|
|
859
|
+
* belongs to its scheduler admission's job or its legacy timestamp identity. Non-timestamp ids retain the longest
|
|
860
|
+
* matching job prefix, so `a` never claims a known `a_b`'s sessions. A
|
|
438
861
|
* missing, locked, or unrecognized state.db answers an empty map; callers send no enrichment.
|
|
439
862
|
*/
|
|
440
863
|
export function latestCronSessionInfo(dbPath, jobIds, completedIds) {
|
|
@@ -454,17 +877,23 @@ export function latestCronSessionInfo(dbPath, jobIds, completedIds) {
|
|
|
454
877
|
const cols = tableColumns(db, 'sessions');
|
|
455
878
|
if (!cols.has('id') || !cols.has('source'))
|
|
456
879
|
return found;
|
|
880
|
+
const admissions = cronAdmissionJobIds(db, true);
|
|
457
881
|
const count = cols.has('message_count') ? 's.message_count' : 'NULL';
|
|
458
882
|
const order = cols.has('started_at') ? 'COALESCE(s.started_at, 0) DESC, s.id DESC' : 's.id DESC';
|
|
459
|
-
const
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
883
|
+
const indexed = tableIndexes(db, 'sessions').has('idx_sessions_source_id') ? ' INDEXED BY idx_sessions_source_id' : '';
|
|
884
|
+
const query = db.prepare(`SELECT s.id AS id, ${count} AS message_count FROM sessions s${indexed} WHERE s.source = 'cron' ORDER BY ${order} LIMIT ? OFFSET ?`);
|
|
885
|
+
const batchSize = wanted.size * 8;
|
|
886
|
+
// Stream older rows only if needed: a busy job must not hide another job's completion link.
|
|
887
|
+
for (const [limit, offset] of [[batchSize, 0], [-1, batchSize]]) {
|
|
888
|
+
for (const row of query.iterate(limit, offset)) {
|
|
889
|
+
const sid = typeof row.id === 'string' ? row.id : '';
|
|
890
|
+
const owner = cronSessionJobId(sid, ids, admissions);
|
|
891
|
+
if (owner === null || !wanted.has(owner) || found.has(owner))
|
|
892
|
+
continue;
|
|
893
|
+
found.set(owner, { session_id: sid, message_count: Number.isFinite(Number(row.message_count)) && row.message_count !== null ? Math.trunc(Number(row.message_count)) : null });
|
|
894
|
+
if (found.size === wanted.size)
|
|
895
|
+
return found;
|
|
896
|
+
}
|
|
468
897
|
}
|
|
469
898
|
return found;
|
|
470
899
|
}
|
|
@@ -472,18 +901,187 @@ export function latestCronSessionInfo(dbPath, jobIds, completedIds) {
|
|
|
472
901
|
return new Map();
|
|
473
902
|
}
|
|
474
903
|
finally {
|
|
475
|
-
db
|
|
904
|
+
finishStateDbRead(db);
|
|
476
905
|
}
|
|
477
906
|
}
|
|
478
|
-
/**
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
907
|
+
/** A source label remains display metadata; canonical receipts and ledgers independently retain write ownership. */
|
|
908
|
+
export const CANONICAL_READ_ONLY_REASON = 'This session is owned by the Agent gateway and is display-only in Talaria.';
|
|
909
|
+
export const OWNERSHIP_UNAVAILABLE_REASON = 'Agent session ownership could not be verified. The session is read-only until storage is readable.';
|
|
910
|
+
export const RETIRED_READ_ONLY_REASON = 'The Agent has retired this session identity. Open or import a new conversation to continue.';
|
|
911
|
+
/** Ownership fences outlive the fully validated localLineages history: even a pruned or broken receipt cannot release a writer. */
|
|
912
|
+
function sessionSources(db, cols, ids) {
|
|
483
913
|
const sources = new Map();
|
|
914
|
+
const canReadRows = cols.has('id') && cols.has('source');
|
|
915
|
+
const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((r) => r.name));
|
|
916
|
+
const walk = canReadRows ? resumeLineageWalker(db, cols) : (sid) => [sid];
|
|
917
|
+
const lineages = new Map(ids.map((id) => [id, walk(id)]));
|
|
918
|
+
const segments = [...new Set([...lineages.values()].flat())];
|
|
919
|
+
const rows = new Map();
|
|
920
|
+
for (let i = 0; canReadRows && i < segments.length; i += 400) {
|
|
921
|
+
const chunk = segments.slice(i, i + 400);
|
|
922
|
+
for (const row of db.prepare(`SELECT id, source, ${cols.has('chat_id') ? 'chat_id' : 'NULL AS chat_id'} FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')})`).all(...chunk))
|
|
923
|
+
rows.set(str(row.id), row);
|
|
924
|
+
}
|
|
925
|
+
const candidates = [...new Set([...segments, ...[...rows.values()].map((r) => str(r.chat_id)).filter(Boolean)])];
|
|
926
|
+
const candidateSet = new Set(candidates);
|
|
927
|
+
const policies = new Map();
|
|
928
|
+
const receiptOwners = new Map();
|
|
929
|
+
const retired = new Set();
|
|
930
|
+
const bindings = new Set();
|
|
931
|
+
const wanted = new Set(segments);
|
|
932
|
+
const policyValue = (value) => {
|
|
933
|
+
try {
|
|
934
|
+
const parsed = JSON.parse(str(value));
|
|
935
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
};
|
|
941
|
+
const rememberPolicy = (owner, policy) => {
|
|
942
|
+
const entry = policy?.entry;
|
|
943
|
+
const target = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry.session_id : null;
|
|
944
|
+
const lineage = Array.isArray(policy?.lineage) ? policy.lineage : [];
|
|
945
|
+
const aliases = [owner, policy?.session_id, policy?.legacy_session_id, target, ...lineage];
|
|
946
|
+
if (!candidateSet.has(owner) && !aliases.some((id) => typeof id === 'string' && wanted.has(id)))
|
|
947
|
+
return;
|
|
948
|
+
policies.set(owner, policy);
|
|
949
|
+
for (const id of aliases)
|
|
950
|
+
if (typeof id === 'string' && wanted.has(id) && !receiptOwners.has(id))
|
|
951
|
+
receiptOwners.set(id, owner);
|
|
952
|
+
};
|
|
953
|
+
if (tables.has('state_meta')) {
|
|
954
|
+
const prefixes = [LOCAL_POLICY_PREFIX, 'gateway.retired_session.v1.', 'gateway.api.binding.v1.'];
|
|
955
|
+
const keys = candidates.flatMap((owner) => prefixes.map((prefix) => `${prefix}${owner}`));
|
|
956
|
+
const metadata = new Map();
|
|
957
|
+
const read = db.prepare('SELECT key, value FROM state_meta WHERE key IN (SELECT value FROM json_each(?))');
|
|
958
|
+
for (let i = 0; i < keys.length; i += 400) {
|
|
959
|
+
for (const row of read.all(JSON.stringify(keys.slice(i, i + 400))))
|
|
960
|
+
metadata.set(row.key, row.value);
|
|
961
|
+
}
|
|
962
|
+
// Keep receipt precedence in candidate order, independent of SQLite's key ordering.
|
|
963
|
+
for (const owner of candidates) {
|
|
964
|
+
const key = `${LOCAL_POLICY_PREFIX}${owner}`;
|
|
965
|
+
if (metadata.has(key))
|
|
966
|
+
rememberPolicy(owner, policyValue(metadata.get(key)));
|
|
967
|
+
if (metadata.has(`gateway.retired_session.v1.${owner}`))
|
|
968
|
+
retired.add(owner);
|
|
969
|
+
if (metadata.has(`gateway.api.binding.v1.${owner}`))
|
|
970
|
+
bindings.add(owner);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const ledgerIds = new Set();
|
|
974
|
+
const ledgerCandidates = JSON.stringify(candidates);
|
|
975
|
+
for (const [table, column] of [['session_admissions', 'target_session_id'], ['worker_executions', 'session_id']]) {
|
|
976
|
+
if (!tables.has(table) || !candidates.length)
|
|
977
|
+
continue;
|
|
978
|
+
const hasLineage = tableColumns(db, table).has('lineage_json');
|
|
979
|
+
for (let i = 0; i < candidates.length; i += 400) {
|
|
980
|
+
const chunk = candidates.slice(i, i + 400);
|
|
981
|
+
const marks = chunk.map(() => '?').join(',');
|
|
982
|
+
for (const row of db.prepare(`SELECT ${column} AS id FROM ${table} WHERE ${column} IN (${marks})`).all(...chunk))
|
|
983
|
+
ledgerIds.add(row.id);
|
|
984
|
+
}
|
|
985
|
+
if (hasLineage) {
|
|
986
|
+
// Bind the complete set once so unrelated history never crosses into JavaScript, even across candidate chunks.
|
|
987
|
+
const scan = db.prepare(`SELECT ${column} AS id, lineage_json FROM ${table} WHERE lineage_json IS NOT NULL AND
|
|
988
|
+
(${column} IN (SELECT value FROM json_each(?)) OR EXISTS
|
|
989
|
+
(SELECT 1 FROM json_each(lineage_json) WHERE value IN (SELECT value FROM json_each(?))))`);
|
|
990
|
+
for (const row of scan.iterate(ledgerCandidates, ledgerCandidates)) {
|
|
991
|
+
const lineage = JSON.parse(row.lineage_json);
|
|
992
|
+
if (!Array.isArray(lineage) || !lineage.every((v) => typeof v === 'string'))
|
|
993
|
+
throw new Error('Unreadable Agent admission lineage');
|
|
994
|
+
ledgerIds.add(row.id);
|
|
995
|
+
for (const sid of lineage)
|
|
996
|
+
ledgerIds.add(sid);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
// Pruned/reset segments can survive only in a receipt, without a row or chat_id to follow.
|
|
1001
|
+
if (tables.has('state_meta') && segments.some((id) => !receiptOwners.has(id) && !ledgerIds.has(id) && !retired.has(id) && !bindings.has(id))) {
|
|
1002
|
+
for (const raw of db.prepare('SELECT key, value FROM state_meta WHERE key GLOB ?').iterate(`${LOCAL_POLICY_PREFIX}*`)) {
|
|
1003
|
+
const policy = policyValue(raw.value);
|
|
1004
|
+
const entry = policy?.entry;
|
|
1005
|
+
const target = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry.session_id : null;
|
|
1006
|
+
if (!policy || typeof policy.session_id !== 'string' || typeof target !== 'string' || (policy.lineage !== undefined && (!Array.isArray(policy.lineage) || !policy.lineage.length || !policy.lineage.every((id) => typeof id === 'string'))))
|
|
1007
|
+
throw new Error('Unreadable Agent ownership receipt');
|
|
1008
|
+
const owner = raw.key.slice(LOCAL_POLICY_PREFIX.length);
|
|
1009
|
+
if (!owner)
|
|
1010
|
+
throw new Error('Unreadable Agent ownership receipt');
|
|
1011
|
+
rememberPolicy(owner, policy);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const kanban = new Set(kanbanOwnedIds(db, cols, new Map([...lineages].filter(([id]) => rows.has(id)))));
|
|
1015
|
+
for (const id of ids) {
|
|
1016
|
+
const row = rows.get(id);
|
|
1017
|
+
let source = kanban.has(id) ? 'kanban' : str(row?.source);
|
|
1018
|
+
let canonical = false;
|
|
1019
|
+
let retiredId = false;
|
|
1020
|
+
let mode;
|
|
1021
|
+
for (const segment of lineages.get(id) ?? [id]) {
|
|
1022
|
+
const chatId = str(rows.get(segment)?.chat_id);
|
|
1023
|
+
for (const owner of new Set([segment, chatId, receiptOwners.get(segment)].filter((v) => Boolean(v)))) {
|
|
1024
|
+
if (retired.has(owner))
|
|
1025
|
+
retiredId = true;
|
|
1026
|
+
if (ledgerIds.has(owner) || bindings.has(owner))
|
|
1027
|
+
canonical = true;
|
|
1028
|
+
if (!policies.has(owner))
|
|
1029
|
+
continue;
|
|
1030
|
+
const policy = policies.get(owner);
|
|
1031
|
+
// A broken receipt never releases its owner or a physical row naming that owner.
|
|
1032
|
+
canonical = true;
|
|
1033
|
+
const origin = policy?.policy;
|
|
1034
|
+
if (origin && typeof origin === 'object' && !Array.isArray(origin)) {
|
|
1035
|
+
const frozen = origin;
|
|
1036
|
+
if (str(frozen.source))
|
|
1037
|
+
source = str(frozen.source);
|
|
1038
|
+
mode ??= frozen.safe_mode === true ? 'safe' : frozen.ignore_user_config === true ? 'ignore_config' : 'normal';
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
const authority = canonical;
|
|
1043
|
+
// LOCAL is the canonical transport. Missing receipts cannot prove it safe to claim.
|
|
1044
|
+
if (lower(source) === 'local')
|
|
1045
|
+
canonical = true;
|
|
1046
|
+
if (!row && !canonical && !retiredId)
|
|
1047
|
+
continue;
|
|
1048
|
+
const owned = { source, canonical: true, ...(mode ? { execution_mode: mode } : {}) };
|
|
1049
|
+
sources.set(id, retiredId && !authority ? { ...owned, reason: RETIRED_READ_ONLY_REASON } : canonical ? owned : source);
|
|
1050
|
+
}
|
|
1051
|
+
return sources;
|
|
1052
|
+
}
|
|
1053
|
+
/** TAL-1040: the request-id prefix Talaria mints for the diagnostic chats it asks an owner to create; its receipt keeps it. */
|
|
1054
|
+
export const WEB_DIAGNOSTIC_REQUEST_PREFIX = 'talaria-web:';
|
|
1055
|
+
/** Owner ids whose creation receipt carries Talaria's request prefix: chats Web started, shown whatever the external-session setting. */
|
|
1056
|
+
export function stateDbWebDiagnosticIds(dbPath) {
|
|
1057
|
+
if (!existsSync(dbPath))
|
|
1058
|
+
return [];
|
|
1059
|
+
let db;
|
|
1060
|
+
try {
|
|
1061
|
+
db = openStateDbReadonly(dbPath);
|
|
1062
|
+
}
|
|
1063
|
+
catch {
|
|
1064
|
+
return [];
|
|
1065
|
+
}
|
|
1066
|
+
try {
|
|
1067
|
+
if (!tableColumns(db, 'state_meta').has('value'))
|
|
1068
|
+
return [];
|
|
1069
|
+
const rows = db.prepare("SELECT key FROM state_meta WHERE key GLOB ? AND json_valid(value) AND json_extract(value, '$.request_id') GLOB ?")
|
|
1070
|
+
.all(`${LOCAL_POLICY_PREFIX}*`, `${WEB_DIAGNOSTIC_REQUEST_PREFIX}*`);
|
|
1071
|
+
return rows.map((row) => row.key.slice(LOCAL_POLICY_PREFIX.length));
|
|
1072
|
+
}
|
|
1073
|
+
catch {
|
|
1074
|
+
return [];
|
|
1075
|
+
}
|
|
1076
|
+
finally {
|
|
1077
|
+
finishStateDbRead(db);
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
/** Batched source/ownership probe. An unreadable authority answers null so callers fail closed. */
|
|
1081
|
+
export function stateDbSessionSources(dbPath, sessionIds) {
|
|
484
1082
|
const ids = [...new Set([...sessionIds].map((s) => s.trim()).filter(Boolean))];
|
|
485
1083
|
if (!ids.length || !existsSync(dbPath))
|
|
486
|
-
return
|
|
1084
|
+
return new Map();
|
|
487
1085
|
let db;
|
|
488
1086
|
try {
|
|
489
1087
|
db = openStateDbReadonly(dbPath);
|
|
@@ -492,22 +1090,82 @@ export function stateDbSessionSources(dbPath, sessionIds) {
|
|
|
492
1090
|
return null;
|
|
493
1091
|
}
|
|
494
1092
|
try {
|
|
495
|
-
|
|
496
|
-
if (!cols.has('id') || !cols.has('source'))
|
|
497
|
-
return sources;
|
|
498
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
499
|
-
const chunk = ids.slice(i, i + 500);
|
|
500
|
-
for (const row of db.prepare(`SELECT id, source FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')})`).all(...chunk))
|
|
501
|
-
sources.set(row.id, str(row.source));
|
|
502
|
-
}
|
|
503
|
-
return sources;
|
|
1093
|
+
return sessionSources(db, tableColumns(db, 'sessions'), ids);
|
|
504
1094
|
}
|
|
505
1095
|
catch {
|
|
506
1096
|
return null;
|
|
507
1097
|
}
|
|
508
1098
|
finally {
|
|
509
|
-
db
|
|
1099
|
+
finishStateDbRead(db);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
/** The same marker parser serves conservative history boundaries and exact-parent ownership walks. */
|
|
1103
|
+
function explicitForkMarkers(row, includeReset) {
|
|
1104
|
+
let cfg = row.model_config;
|
|
1105
|
+
if (typeof cfg === 'string') {
|
|
1106
|
+
try {
|
|
1107
|
+
cfg = JSON.parse(cfg);
|
|
1108
|
+
}
|
|
1109
|
+
catch {
|
|
1110
|
+
return [];
|
|
1111
|
+
}
|
|
510
1112
|
}
|
|
1113
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
|
|
1114
|
+
return [];
|
|
1115
|
+
const c = cfg;
|
|
1116
|
+
return [c._branched_from, c._delegate_from, ...(includeReset ? [c._reset_from] : [])].filter((m) => m !== undefined && m !== null);
|
|
1117
|
+
}
|
|
1118
|
+
/** Python `_is_explicit_fork_child_row`: a tool child, or a branch/delegate (`includeReset`: also reset) fork of its parent. */
|
|
1119
|
+
function isExplicitForkChild(row, includeReset) {
|
|
1120
|
+
if (str(row.source) === 'tool')
|
|
1121
|
+
return true;
|
|
1122
|
+
const markers = explicitForkMarkers(row, includeReset);
|
|
1123
|
+
const parentId = row.parent_session_id;
|
|
1124
|
+
return parentId ? markers.includes(parentId) : markers.length > 0;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Python `_resume_lineage_ids`: `sid` and the compression ancestors a resume loads, root first. Each hop's parent ended
|
|
1128
|
+
* `compression` and the child is no branch, reset, delegate or tool fork. The walk stops at a cycle or
|
|
1129
|
+
* `RESUME_LINEAGE_CAP` ids; without the lineage columns it is `[sid]`.
|
|
1130
|
+
*/
|
|
1131
|
+
function resumeLineageWalker(db, cols) {
|
|
1132
|
+
if (!['parent_session_id', 'end_reason'].every((c) => cols.has(c)))
|
|
1133
|
+
return (sid) => [sid];
|
|
1134
|
+
const select = db.prepare(`SELECT id, source, parent_session_id, end_reason, ${cols.has('model_config') ? 'model_config' : 'NULL AS model_config'} FROM sessions WHERE id = ?`);
|
|
1135
|
+
return (sid) => {
|
|
1136
|
+
const chain = [sid];
|
|
1137
|
+
let current = select.get(sid);
|
|
1138
|
+
if (!current || isExplicitForkChild(current, false))
|
|
1139
|
+
return chain;
|
|
1140
|
+
while (chain.length < RESUME_LINEAGE_CAP) {
|
|
1141
|
+
const parentId = str(current.parent_session_id);
|
|
1142
|
+
if (!parentId || chain.includes(parentId) || isExplicitForkChild(current, true))
|
|
1143
|
+
break;
|
|
1144
|
+
const parent = select.get(parentId);
|
|
1145
|
+
if (str(parent?.end_reason) !== 'compression')
|
|
1146
|
+
break;
|
|
1147
|
+
chain.push(parentId);
|
|
1148
|
+
current = parent;
|
|
1149
|
+
}
|
|
1150
|
+
return chain.reverse();
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* TAL-793 (Agent `is_kanban_owned_session`): the `ids` whose compression lineage has a `kanban` `source` or
|
|
1155
|
+
* `created_source` (schema v31; `source` alone decides before it), a Kanban worker's transcript only its dispatcher continues.
|
|
1156
|
+
*/
|
|
1157
|
+
function kanbanOwnedIds(db, cols, lineages) {
|
|
1158
|
+
if (!lineages.size)
|
|
1159
|
+
return [];
|
|
1160
|
+
const segments = [...new Set([...lineages.values()].flat())];
|
|
1161
|
+
const match = cols.has('created_source') ? "(source = 'kanban' OR created_source = 'kanban')" : "source = 'kanban'";
|
|
1162
|
+
const kanban = new Set();
|
|
1163
|
+
for (let i = 0; i < segments.length; i += 500) {
|
|
1164
|
+
const chunk = segments.slice(i, i + 500);
|
|
1165
|
+
for (const row of db.prepare(`SELECT id FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')}) AND ${match}`).all(...chunk))
|
|
1166
|
+
kanban.add(row.id);
|
|
1167
|
+
}
|
|
1168
|
+
return [...lineages].filter(([, lineage]) => lineage.some((sid) => kanban.has(sid))).map(([id]) => id);
|
|
511
1169
|
}
|
|
512
1170
|
/** Python `state_db_has_session`: true only when `sid` is a row of the sessions table; a missing or unreadable db is false. */
|
|
513
1171
|
export function stateDbHasSession(dbPath, sid) {
|
|
@@ -530,7 +1188,33 @@ export function stateDbHasSession(dbPath, sid) {
|
|
|
530
1188
|
return false;
|
|
531
1189
|
}
|
|
532
1190
|
finally {
|
|
533
|
-
db
|
|
1191
|
+
finishStateDbRead(db);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
/** True when a session has a live message; never materializes its transcript or compaction archive. */
|
|
1195
|
+
export function stateDbSessionHasMessages(dbPath, sid) {
|
|
1196
|
+
const id = sid.trim();
|
|
1197
|
+
if (!id || !existsSync(dbPath))
|
|
1198
|
+
return false;
|
|
1199
|
+
let db;
|
|
1200
|
+
try {
|
|
1201
|
+
db = openStateDbReadonly(dbPath);
|
|
1202
|
+
}
|
|
1203
|
+
catch {
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
1206
|
+
try {
|
|
1207
|
+
const cols = tableColumns(db, 'messages');
|
|
1208
|
+
if (!cols.has('session_id'))
|
|
1209
|
+
return false;
|
|
1210
|
+
const active = cols.has('active') ? ' AND (active IS NULL OR active != 0)' : '';
|
|
1211
|
+
return db.prepare(`SELECT 1 FROM messages WHERE session_id = ?${active} LIMIT 1`).get(id) !== undefined;
|
|
1212
|
+
}
|
|
1213
|
+
catch {
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
finally {
|
|
1217
|
+
finishStateDbRead(db);
|
|
534
1218
|
}
|
|
535
1219
|
}
|
|
536
1220
|
/**
|
|
@@ -541,7 +1225,7 @@ export function stateDbHasSession(dbPath, sid) {
|
|
|
541
1225
|
export function cheapChangeFingerprint(dbPath, onError) {
|
|
542
1226
|
try {
|
|
543
1227
|
const parts = [];
|
|
544
|
-
const db = openStateDbReadonly(dbPath);
|
|
1228
|
+
const db = openStateDbReadonly(dbPath, STATE_DB_FINGERPRINT_BUSY_TIMEOUT_MS);
|
|
545
1229
|
try {
|
|
546
1230
|
for (const table of ['sessions', 'messages']) {
|
|
547
1231
|
try {
|
|
@@ -553,7 +1237,7 @@ export function cheapChangeFingerprint(dbPath, onError) {
|
|
|
553
1237
|
}
|
|
554
1238
|
}
|
|
555
1239
|
finally {
|
|
556
|
-
db
|
|
1240
|
+
finishStateDbRead(db);
|
|
557
1241
|
}
|
|
558
1242
|
for (const path of [dbPath, `${dbPath}-wal`]) {
|
|
559
1243
|
try {
|
|
@@ -585,22 +1269,28 @@ export function cheapChangeFingerprint(dbPath, onError) {
|
|
|
585
1269
|
return null;
|
|
586
1270
|
}
|
|
587
1271
|
}
|
|
588
|
-
|
|
1272
|
+
const STATE_DB_WORKER_LANES = ['listing', 'authority', 'session'];
|
|
1273
|
+
const listingWorkers = new Map();
|
|
1274
|
+
const listingReadScope = new AsyncLocalStorage();
|
|
1275
|
+
/** Keep a sidebar phase's nested reads on its lane without changing interactive callers' defaults. */
|
|
1276
|
+
export function withStateDbListingReads(read) { return listingReadScope.run('listing', read); }
|
|
589
1277
|
let nextCallId = 0;
|
|
590
|
-
/** Set
|
|
1278
|
+
/** Set until every worker exits during shutdown; calls then reject instead of starting another worker. */
|
|
591
1279
|
let stoppingWorker = false;
|
|
592
1280
|
/** Reject every call still waiting on `current` and forget it, so the next call starts a fresh worker. */
|
|
593
1281
|
function retireListingWorker(current, error) {
|
|
594
|
-
|
|
595
|
-
|
|
1282
|
+
for (const [lane, worker] of listingWorkers)
|
|
1283
|
+
if (worker === current)
|
|
1284
|
+
listingWorkers.delete(lane);
|
|
596
1285
|
for (const call of current.pending.values())
|
|
597
1286
|
call.reject(error);
|
|
598
1287
|
current.pending.clear();
|
|
599
1288
|
}
|
|
600
|
-
/**
|
|
601
|
-
function startedListingWorker() {
|
|
602
|
-
|
|
603
|
-
|
|
1289
|
+
/** Each lane lazily starts one long-lived worker and keeps its own FIFO and read-only handle cache. */
|
|
1290
|
+
function startedListingWorker(lane) {
|
|
1291
|
+
const existing = listingWorkers.get(lane);
|
|
1292
|
+
if (existing)
|
|
1293
|
+
return existing;
|
|
604
1294
|
// Source runs (vitest) load the TypeScript worker; the build runs the compiled one beside this module.
|
|
605
1295
|
const ext = import.meta.url.endsWith('.ts') ? 'ts' : 'js';
|
|
606
1296
|
const current = { worker: new Worker(new URL(`./state-db-worker.${ext}`, import.meta.url)), pending: new Map() };
|
|
@@ -616,20 +1306,26 @@ function startedListingWorker() {
|
|
|
616
1306
|
});
|
|
617
1307
|
current.worker.on('error', (error) => { retireListingWorker(current, error); });
|
|
618
1308
|
current.worker.on('exit', (code) => { retireListingWorker(current, new Error(`state.db worker exited with code ${String(code)}`)); });
|
|
619
|
-
|
|
1309
|
+
listingWorkers.set(lane, current);
|
|
620
1310
|
return current;
|
|
621
1311
|
}
|
|
622
|
-
function callListingWorker(call) {
|
|
1312
|
+
function callListingWorker(call, lane) {
|
|
623
1313
|
// A multi-read caller the stop interrupted goes on to its next read; that read must not restart the worker.
|
|
624
1314
|
if (stoppingWorker)
|
|
625
1315
|
return Promise.reject(new Error('state.db worker stopped'));
|
|
626
|
-
const
|
|
1316
|
+
const target = lane ?? listingReadScope.getStore() ?? (call.op === 'session' ? SESSION_READ_LANES[call.read] : call.op === 'sources' || call.op === 'existing' ? 'authority' : 'listing');
|
|
1317
|
+
const current = startedListingWorker(target);
|
|
627
1318
|
const id = ++nextCallId;
|
|
1319
|
+
const timing = startupTimingScope.getStore();
|
|
628
1320
|
return new Promise((resolve, reject) => {
|
|
629
|
-
current.pending.set(id, { resolve
|
|
1321
|
+
current.pending.set(id, { resolve: (reply) => {
|
|
1322
|
+
if (reply.timing)
|
|
1323
|
+
timing?.workerRead(target, call.op === 'session' ? call.read : call.op, reply.timing);
|
|
1324
|
+
resolve(reply);
|
|
1325
|
+
}, reject });
|
|
630
1326
|
// A call in flight keeps the process alive until it settles; an idle worker never does.
|
|
631
1327
|
current.worker.ref();
|
|
632
|
-
current.worker.postMessage({ ...call, id, delayMs: readDelayMs });
|
|
1328
|
+
current.worker.postMessage({ ...call, id, delayMs: readDelayMs, queuedAtNs: process.hrtime.bigint() });
|
|
633
1329
|
});
|
|
634
1330
|
}
|
|
635
1331
|
/** `readImportableAgentSessionRows` on the listing worker, so a slow `state.db` never blocks the event loop. Rejects when the worker fails. */
|
|
@@ -642,6 +1338,15 @@ export async function readImportableAgentSessionRowsAsync(dbPath, opts = {}) {
|
|
|
642
1338
|
throw new Error(reply.error);
|
|
643
1339
|
return reply.value;
|
|
644
1340
|
}
|
|
1341
|
+
/** One worker crossing for all sidebar windows and their storage health. */
|
|
1342
|
+
export async function readImportableAgentSessionRowBatchesAsync(dbPath, queries) {
|
|
1343
|
+
const reply = await callListingWorker({ op: 'rowBatches', dbPath, queries: queries.map((opts) => ({ ...opts, log: undefined })) });
|
|
1344
|
+
for (const line of reply.logs)
|
|
1345
|
+
queries[0]?.log?.(line);
|
|
1346
|
+
if (reply.error !== undefined)
|
|
1347
|
+
throw new Error(reply.error);
|
|
1348
|
+
return reply.value;
|
|
1349
|
+
}
|
|
645
1350
|
/** `cheapChangeFingerprint` on the listing worker; a worker failure reports as a failed change check. */
|
|
646
1351
|
export async function cheapChangeFingerprintAsync(dbPath, onError) {
|
|
647
1352
|
let reply;
|
|
@@ -656,8 +1361,19 @@ export async function cheapChangeFingerprintAsync(dbPath, onError) {
|
|
|
656
1361
|
onError?.(reason, new Error(message));
|
|
657
1362
|
return reply.value;
|
|
658
1363
|
}
|
|
659
|
-
/** `
|
|
1364
|
+
/** `stateDbStorage` on the listing worker; a worker failure is `unavailable`. */
|
|
1365
|
+
export async function stateDbStorageAsync(dbPath) {
|
|
1366
|
+
try {
|
|
1367
|
+
return (await callListingWorker({ op: 'storage', dbPath })).value;
|
|
1368
|
+
}
|
|
1369
|
+
catch (error) {
|
|
1370
|
+
return stateDbReadFailure(error);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
/** `stateDbSessionSources` on the authority worker; a worker failure is an unreadable state.db (null), so owners stay locked. Without a state.db it answers here, opening nothing. */
|
|
660
1374
|
export async function stateDbSessionSourcesAsync(dbPath, sessionIds) {
|
|
1375
|
+
if (!existsSync(dbPath))
|
|
1376
|
+
return stateDbSessionSources(dbPath, sessionIds);
|
|
661
1377
|
try {
|
|
662
1378
|
return (await callListingWorker({ op: 'sources', dbPath, ids: [...sessionIds] })).value;
|
|
663
1379
|
}
|
|
@@ -665,7 +1381,16 @@ export async function stateDbSessionSourcesAsync(dbPath, sessionIds) {
|
|
|
665
1381
|
return null;
|
|
666
1382
|
}
|
|
667
1383
|
}
|
|
668
|
-
/** `
|
|
1384
|
+
/** `stateDbWebDiagnosticIds` on the listing worker; a failed read lists none, so the setting still governs every other row. */
|
|
1385
|
+
export async function stateDbWebDiagnosticIdsAsync(dbPath) {
|
|
1386
|
+
try {
|
|
1387
|
+
return (await callListingWorker({ op: 'webDiagnostic', dbPath })).value;
|
|
1388
|
+
}
|
|
1389
|
+
catch {
|
|
1390
|
+
return [];
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/** `agentSessionRowsExisting` on the authority worker; a worker failure assumes every row present, so nothing is pruned. */
|
|
669
1394
|
export async function agentSessionRowsExistingAsync(dbPath, sessionIds) {
|
|
670
1395
|
const ids = [...sessionIds];
|
|
671
1396
|
try {
|
|
@@ -675,19 +1400,60 @@ export async function agentSessionRowsExistingAsync(dbPath, sessionIds) {
|
|
|
675
1400
|
return new Set(ids.map((s) => s.trim()).filter(Boolean));
|
|
676
1401
|
}
|
|
677
1402
|
}
|
|
678
|
-
/**
|
|
679
|
-
|
|
680
|
-
|
|
1403
|
+
/**
|
|
1404
|
+
* TAL-923: the single-session readers behind chat start, session detail, and the mutation gates, run on the worker.
|
|
1405
|
+
* TAL-1162: and the transcript reads behind turn history, settlement, and the detail's page tail.
|
|
1406
|
+
* TAL-1256: shared cron admission identities and completion links use this same dispatcher.
|
|
1407
|
+
*/
|
|
1408
|
+
export const SESSION_READS = { stateDbHasSession, stateDbSessionHasMessages, insightsSessionRows, countConversationRounds, stateDbTailToolContent, stateDbLineageReport, stateDbCronJobIds, latestCronSessionInfo, stateDbLocalTargetRead, stateDbSessionRow, stateDbSessionMessages, stateDbCompressionLineage, stateDbSessionRead, stateDbSessionTailRead, stateDbSettlementRead };
|
|
1409
|
+
/** Exhaustive policy: authority never waits for transcript materialization; sidebar-scoped probes stay on listing. */
|
|
1410
|
+
const SESSION_READ_LANES = {
|
|
1411
|
+
stateDbHasSession: 'authority', stateDbSessionHasMessages: 'authority', stateDbLocalTargetRead: 'authority', stateDbSessionRow: 'authority',
|
|
1412
|
+
insightsSessionRows: 'session', countConversationRounds: 'session', stateDbTailToolContent: 'session', stateDbLineageReport: 'session',
|
|
1413
|
+
stateDbCronJobIds: 'session', latestCronSessionInfo: 'session', stateDbSessionMessages: 'session', stateDbCompressionLineage: 'session',
|
|
1414
|
+
stateDbSessionRead: 'session', stateDbSessionTailRead: 'session', stateDbSettlementRead: 'session',
|
|
1415
|
+
};
|
|
1416
|
+
/**
|
|
1417
|
+
* A `SESSION_READS` reader on its authority or transcript lane, so expensive reads cannot hold up authority. Each reader
|
|
1418
|
+
* already answers a failed read with `fallback`'s value; a worker failure answers it too. Without a state.db the reader
|
|
1419
|
+
* answers here, as it opens nothing then.
|
|
1420
|
+
*/
|
|
1421
|
+
export async function stateDbSessionReadAsync(fallback, read, ...args) {
|
|
1422
|
+
if (!existsSync(args[0]))
|
|
1423
|
+
return SESSION_READS[read](...args);
|
|
1424
|
+
try {
|
|
1425
|
+
const reply = await callListingWorker({ op: 'session', read, args });
|
|
1426
|
+
return reply.error === undefined ? reply.value : fallback;
|
|
1427
|
+
}
|
|
1428
|
+
catch {
|
|
1429
|
+
return fallback;
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
/** Test hook: crash one lane, or all when omitted, while its calls are in flight. */
|
|
1433
|
+
export function crashStateDbWorkerForTests(lane) {
|
|
1434
|
+
return Promise.all((lane ? [lane] : STATE_DB_WORKER_LANES).map((target) => callListingWorker({ op: 'crash' }, target)));
|
|
681
1435
|
}
|
|
682
|
-
/** Stop
|
|
1436
|
+
/** Stop every worker (server shutdown). Calls in flight reject; later calls start fresh workers. */
|
|
683
1437
|
export async function stopStateDbWorker() {
|
|
684
|
-
|
|
685
|
-
|
|
1438
|
+
closeStateDbs();
|
|
1439
|
+
const workers = [...listingWorkers.values()];
|
|
1440
|
+
if (!workers.length)
|
|
686
1441
|
return;
|
|
687
1442
|
stoppingWorker = true;
|
|
688
1443
|
try {
|
|
689
|
-
|
|
690
|
-
|
|
1444
|
+
await Promise.all(workers.map(async (current) => {
|
|
1445
|
+
retireListingWorker(current, new Error('state.db worker stopped'));
|
|
1446
|
+
const exited = new Promise((resolve) => { current.worker.once('exit', () => { resolve(); }); });
|
|
1447
|
+
// Idle workers close their cache through process.exit; a blocked read still cannot delay shutdown.
|
|
1448
|
+
const force = setTimeout(() => { void current.worker.terminate(); }, 100);
|
|
1449
|
+
try {
|
|
1450
|
+
current.worker.postMessage({ op: 'stop', id: 0, delayMs: 0 });
|
|
1451
|
+
await exited;
|
|
1452
|
+
}
|
|
1453
|
+
finally {
|
|
1454
|
+
clearTimeout(force);
|
|
1455
|
+
}
|
|
1456
|
+
}));
|
|
691
1457
|
}
|
|
692
1458
|
finally {
|
|
693
1459
|
stoppingWorker = false;
|
|
@@ -696,16 +1462,23 @@ export async function stopStateDbWorker() {
|
|
|
696
1462
|
// ── message reader ────────────────────────────────────────────────────────
|
|
697
1463
|
const STATE_DB_CONTENT_JSON_PREFIX = '\0json:';
|
|
698
1464
|
// `display_kind` / `display_metadata`: the Agent's own tag for rows it writes itself (steers, delivered notifications; TAL-371).
|
|
699
|
-
const OPTIONAL_MESSAGE_COLUMNS = ['tool_call_id', 'tool_calls', 'tool_name', 'reasoning', 'reasoning_details', 'codex_reasoning_items', 'reasoning_content', 'codex_message_items', 'api_content', 'display_kind', 'display_metadata'];
|
|
700
|
-
const JSON_MESSAGE_COLUMNS = new Set(['tool_calls', 'reasoning_details', 'codex_reasoning_items', 'codex_message_items', 'display_metadata']);
|
|
1465
|
+
const OPTIONAL_MESSAGE_COLUMNS = ['tool_call_id', 'tool_calls', 'tool_name', 'reasoning', 'reasoning_details', 'codex_reasoning_items', 'reasoning_content', 'codex_message_items', 'api_content', 'display_kind', 'display_metadata', 'absorbed_message_uids', '_compressed_summary'];
|
|
1466
|
+
const JSON_MESSAGE_COLUMNS = new Set(['tool_calls', 'reasoning_details', 'codex_reasoning_items', 'codex_message_items', 'display_metadata', 'absorbed_message_uids']);
|
|
701
1467
|
/** Python `_decode_state_db_content`: the Agent's sentinel-prefixed structured content, left untouched otherwise. */
|
|
702
1468
|
function decodeStateDbContent(value) {
|
|
703
1469
|
const text = Buffer.isBuffer(value) ? value.toString('utf8') : value;
|
|
704
1470
|
if (typeof text !== 'string' || !text.startsWith(STATE_DB_CONTENT_JSON_PREFIX))
|
|
705
1471
|
return text;
|
|
706
1472
|
try {
|
|
707
|
-
const
|
|
708
|
-
|
|
1473
|
+
const payload = text.slice(STATE_DB_CONTENT_JSON_PREFIX.length);
|
|
1474
|
+
let unsafeInteger = false;
|
|
1475
|
+
const decoded = JSON.parse(payload, (_key, value, context) => {
|
|
1476
|
+
if (typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context?.source ?? ''))
|
|
1477
|
+
unsafeInteger = true;
|
|
1478
|
+
return value;
|
|
1479
|
+
});
|
|
1480
|
+
// Python keeps arbitrary-precision integers. Retain the JSON text rather than silently rounding any of them.
|
|
1481
|
+
return unsafeInteger ? payload : decoded;
|
|
709
1482
|
}
|
|
710
1483
|
catch {
|
|
711
1484
|
return text;
|
|
@@ -721,20 +1494,39 @@ function jsonLoadsIfString(value) {
|
|
|
721
1494
|
return value;
|
|
722
1495
|
}
|
|
723
1496
|
}
|
|
1497
|
+
// Agent v0.22.3's ordinary kept-tail handoff. Only its explicit flag and exact wrapper prove a carrier.
|
|
1498
|
+
const PRIOR_CONTEXT_HEADER = '[PRIOR CONTEXT — for reference only; not a new message]';
|
|
1499
|
+
const COMPACTION_SUMMARY_DELIMITER = '\n\n[END OF PRIOR CONTEXT — COMPACTION SUMMARY BELOW]\n\n';
|
|
1500
|
+
function compactionCarrier(row) {
|
|
1501
|
+
if (row._compressed_summary !== 1 && row._compressed_summary !== true)
|
|
1502
|
+
return null;
|
|
1503
|
+
const content = decodeStateDbContent(row.content);
|
|
1504
|
+
if ((row.role !== 'user' && row.role !== 'assistant') || typeof content !== 'string' || !content.startsWith(`${PRIOR_CONTEXT_HEADER}\n`))
|
|
1505
|
+
return null;
|
|
1506
|
+
const boundary = content.indexOf(COMPACTION_SUMMARY_DELIMITER, PRIOR_CONTEXT_HEADER.length);
|
|
1507
|
+
if (boundary < 0)
|
|
1508
|
+
return null;
|
|
1509
|
+
// Remove only the separators the Agent added; preserve the kept reply's own whitespace.
|
|
1510
|
+
const prior = content.slice(PRIOR_CONTEXT_HEADER.length + 1, boundary);
|
|
1511
|
+
const summary = content.slice(boundary + COMPACTION_SUMMARY_DELIMITER.length).trim();
|
|
1512
|
+
return prior && summary ? { prior, summary } : null;
|
|
1513
|
+
}
|
|
724
1514
|
/** Python `_project_state_db_message`: one row → WebUI message (`tool_name → name`, empty optionals omitted). */
|
|
725
1515
|
function projectStateDbMessage(row, hasId) {
|
|
726
|
-
const msg = { role: row.role, content: decodeStateDbContent(row.content), timestamp: row.timestamp };
|
|
1516
|
+
const msg = { role: row.role, content: compactionCarrier(row)?.prior ?? decodeStateDbContent(row.content), timestamp: row.timestamp };
|
|
727
1517
|
for (const col of OPTIONAL_MESSAGE_COLUMNS) {
|
|
728
|
-
if (!(col in row))
|
|
1518
|
+
if (col === '_compressed_summary' || !(col in row))
|
|
729
1519
|
continue;
|
|
730
1520
|
const value = row[col];
|
|
731
1521
|
if (value === null || value === undefined || value === '')
|
|
732
1522
|
continue;
|
|
733
|
-
msg[col] = JSON_MESSAGE_COLUMNS.has(col) ? jsonLoadsIfString(value) : value;
|
|
1523
|
+
msg[col === 'absorbed_message_uids' ? '_absorbed_message_uids' : col] = JSON_MESSAGE_COLUMNS.has(col) ? jsonLoadsIfString(value) : value;
|
|
734
1524
|
}
|
|
735
1525
|
// TAL-493: the merge tells rows committed after the session's last read by this id.
|
|
736
1526
|
if (hasId && row.id !== null && row.id !== undefined)
|
|
737
1527
|
msg._state_db_row_id = row.id;
|
|
1528
|
+
if (typeof row.message_uid === 'string' && row.message_uid.length)
|
|
1529
|
+
msg._state_db_message_uid = row.message_uid;
|
|
738
1530
|
if (msg.role === 'tool' && msg.tool_name && !msg.name)
|
|
739
1531
|
msg.name = msg.tool_name;
|
|
740
1532
|
return msg;
|
|
@@ -753,6 +1545,76 @@ function stateDbGeneration(dbPath) {
|
|
|
753
1545
|
return null;
|
|
754
1546
|
}
|
|
755
1547
|
}
|
|
1548
|
+
// Agent v0.21.6's exact in-flight request wrapper. Unknown rewrites are not copy evidence.
|
|
1549
|
+
const INFLIGHT_REPLAY_HEADER = '[STILL IN PROGRESS — this is the active request, restated after the compaction boundary because it was not finished yet. Continue it; do not start over.]';
|
|
1550
|
+
/** Compare the kept payload after the Agent's exact request/carrier wrappers and stale api_content are removed. */
|
|
1551
|
+
function replayPayload(row) {
|
|
1552
|
+
const message = projectStateDbMessage(row, false);
|
|
1553
|
+
delete message.timestamp;
|
|
1554
|
+
const copyKey = JSON.stringify(message);
|
|
1555
|
+
let content = message.content;
|
|
1556
|
+
let replay = false;
|
|
1557
|
+
if (row.role === 'user') {
|
|
1558
|
+
if (typeof content === 'string') {
|
|
1559
|
+
replay = content.startsWith(`${INFLIGHT_REPLAY_HEADER}\n`);
|
|
1560
|
+
content = (replay ? content.slice(INFLIGHT_REPLAY_HEADER.length + 1) : content).trim();
|
|
1561
|
+
}
|
|
1562
|
+
else if (Array.isArray(content)) {
|
|
1563
|
+
// Multimodal replays prepend a text block and retain all original parts, including media.
|
|
1564
|
+
const parts = content;
|
|
1565
|
+
let start = 0;
|
|
1566
|
+
while (start < parts.length) {
|
|
1567
|
+
const first = parts[start];
|
|
1568
|
+
if (!first || typeof first !== 'object' || Array.isArray(first))
|
|
1569
|
+
break;
|
|
1570
|
+
const part = first;
|
|
1571
|
+
if (part.type !== 'text' || part.text !== `${INFLIGHT_REPLAY_HEADER}\n` || Object.keys(part).some((k) => k !== 'type' && k !== 'text'))
|
|
1572
|
+
break;
|
|
1573
|
+
start += 1;
|
|
1574
|
+
}
|
|
1575
|
+
replay = start > 0;
|
|
1576
|
+
content = parts.slice(start);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
message.content = content;
|
|
1580
|
+
delete message.api_content; // _reappend_inflight_user_task drops this stale model-facing sidecar.
|
|
1581
|
+
return { copyKey, replayKey: JSON.stringify(message), replay, carrier: compactionCarrier(row)?.summary ?? null };
|
|
1582
|
+
}
|
|
1583
|
+
/**
|
|
1584
|
+
* Compare adjacent generations, never all rows of one UID with its oldest archive. The archive retains
|
|
1585
|
+
* authored payload changes even after active/compacted flags change. Only unchanged timestamp copies or
|
|
1586
|
+
* exact request restatements inherit identity; edits (including A -> B -> A) start at their own row ID.
|
|
1587
|
+
*/
|
|
1588
|
+
function replayIdentities(db, chain, rows, available) {
|
|
1589
|
+
const identities = new Map();
|
|
1590
|
+
// Tool-result text can be pruned without changing its tool-call identity; retain the existing rule.
|
|
1591
|
+
const uids = [...new Set(rows.filter((row) => row.role !== 'tool').map((row) => row.message_uid).filter((uid) => typeof uid === 'string' && uid.length > 0))];
|
|
1592
|
+
if (!uids.length)
|
|
1593
|
+
return identities;
|
|
1594
|
+
const liveIds = new Set(rows.map((row) => row.id));
|
|
1595
|
+
const previous = new Map();
|
|
1596
|
+
const columns = ['id', 'message_uid', 'active', 'compacted', 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
|
|
1597
|
+
// One ordered scan of the selected sessions, filtering against a materialized UID set. Stream the rows;
|
|
1598
|
+
// memory holds only the preceding payload per live UID, not the accumulated archive. NULL/nonstandard
|
|
1599
|
+
// active flags take exactly the same path and do not depend on the Agent's partial active=1 UID index.
|
|
1600
|
+
const history = db.prepare(`SELECT ${columns.join(', ')} FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')}) AND message_uid IN (SELECT value FROM json_each(?)) ORDER BY id`);
|
|
1601
|
+
for (const row of history.iterate(...chain, JSON.stringify(uids))) {
|
|
1602
|
+
const uid = row.message_uid;
|
|
1603
|
+
const payload = replayPayload(row);
|
|
1604
|
+
const prior = previous.get(uid);
|
|
1605
|
+
const copy = payload.copyKey === prior?.copyKey && row.timestamp === prior.timestamp;
|
|
1606
|
+
const restatement = payload.replayKey === prior?.replayKey && ((payload.replay && (prior.active !== 0 || prior.compacted === 1)) ||
|
|
1607
|
+
(payload.carrier && row.timestamp === prior.timestamp));
|
|
1608
|
+
const identity = prior && (copy || restatement) ? prior.identity : Number(row.id);
|
|
1609
|
+
const changed = prior ? (copy || restatement ? prior.changed : true) : false;
|
|
1610
|
+
const sourceKey = prior && (copy || restatement) ? prior.sourceKey : stateDbMessageFingerprint(projectStateDbMessage(row, false));
|
|
1611
|
+
const summaryId = payload.carrier && payload.carrier === prior?.carrier && row.timestamp === prior.timestamp ? prior.summaryId : Number(row.id);
|
|
1612
|
+
if (prior && row.role !== 'tool' && liveIds.has(row.id))
|
|
1613
|
+
identities.set(Number(row.id), { id: identity, changed, sourceKey, summaryId });
|
|
1614
|
+
previous.set(uid, { ...payload, identity, changed, sourceKey, summaryId, timestamp: row.timestamp, active: row.active, compacted: row.compacted });
|
|
1615
|
+
}
|
|
1616
|
+
return identities;
|
|
1617
|
+
}
|
|
756
1618
|
export function stateDbSessionMessages(dbPath, sid, opts = {}) {
|
|
757
1619
|
return stateDbSessionRead(dbPath, sid, opts).rows;
|
|
758
1620
|
}
|
|
@@ -761,7 +1623,24 @@ export function stateDbSessionMessages(dbPath, sid, opts = {}) {
|
|
|
761
1623
|
* (TAL-493: an empty id-capable read is a baseline of 0; a missing database or a failed read says nothing).
|
|
762
1624
|
*/
|
|
763
1625
|
export function stateDbSessionRead(dbPath, sid, opts = {}) {
|
|
764
|
-
|
|
1626
|
+
return readStateDbSession(dbPath, sid, opts);
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* A settlement's reads in one request: the transcript under `recorded` (the session's `state_db_lineage`), the lineage
|
|
1630
|
+
* the Agent's compression rotations give it now, and, when that differs from `recorded`, the transcript under it.
|
|
1631
|
+
*/
|
|
1632
|
+
export function stateDbSettlementRead(dbPath, sid, recorded) {
|
|
1633
|
+
const read = stateDbSessionRead(dbPath, sid, { stitch: false, lineage: recorded?.[0] === sid ? recorded : null });
|
|
1634
|
+
const lineage = stateDbCompressionLineage(dbPath, sid);
|
|
1635
|
+
const moved = JSON.stringify(lineage) !== JSON.stringify(recorded ?? [sid]);
|
|
1636
|
+
return { ...read, lineage, rotated: moved ? stateDbSessionRead(dbPath, sid, { stitch: false, lineage: lineage.length > 1 && lineage[0] === sid ? lineage : null }) : null };
|
|
1637
|
+
}
|
|
1638
|
+
/** The same snapshot and provenance projection, restricted to physical rows past the seen marker. */
|
|
1639
|
+
export function stateDbSessionTailRead(dbPath, sid, afterId, opts = {}) {
|
|
1640
|
+
return readStateDbSession(dbPath, sid, { ...opts, stitch: false, afterId });
|
|
1641
|
+
}
|
|
1642
|
+
function readStateDbSession(dbPath, sid, opts) {
|
|
1643
|
+
const none = { rows: [], idCapable: false, ok: false, reason: null, generation: null };
|
|
765
1644
|
const id = sid.trim();
|
|
766
1645
|
if (!id || !existsSync(dbPath))
|
|
767
1646
|
return none;
|
|
@@ -769,24 +1648,26 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
|
|
|
769
1648
|
try {
|
|
770
1649
|
db = openStateDbReadonly(dbPath);
|
|
771
1650
|
}
|
|
772
|
-
catch {
|
|
773
|
-
return none;
|
|
1651
|
+
catch (error) {
|
|
1652
|
+
return { ...none, reason: stateDbReadFailure(error).reason };
|
|
774
1653
|
}
|
|
775
1654
|
try {
|
|
776
1655
|
const available = tableColumns(db, 'messages');
|
|
777
1656
|
if (!['role', 'content', 'timestamp'].every((c) => available.has(c)))
|
|
778
1657
|
return { ...none, ok: available.size > 0 };
|
|
779
1658
|
const hasId = available.has('id');
|
|
780
|
-
const selected = [...(hasId ? ['id'] : []), 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
|
|
1659
|
+
const selected = [...(hasId ? ['id'] : []), ...(available.has('message_uid') ? ['message_uid'] : []), 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
|
|
781
1660
|
// TAL-529: an explicit lineage (the session and its compression continuations) replaces the parent walk.
|
|
782
|
-
const
|
|
783
|
-
|
|
1661
|
+
const local = localLineages(db, id).get(id);
|
|
1662
|
+
const chain = local ? (opts.stitch === false ? [local.ids.at(-1) ?? id] : [...local.context]) : opts.lineage?.length ? [...opts.lineage] : [id];
|
|
1663
|
+
if (!local && !opts.lineage?.length && (opts.stitch ?? true)) {
|
|
784
1664
|
const sessionCols = tableColumns(db, 'sessions');
|
|
785
1665
|
if (['parent_session_id', 'end_reason', 'started_at', 'source'].every((c) => sessionCols.has(c))) {
|
|
786
|
-
const
|
|
1666
|
+
const extra = ['model_config', 'session_source'].map((c) => sessionCols.has(c) ? c : `NULL AS ${c}`).join(', ');
|
|
1667
|
+
const select = db.prepare(`SELECT id, source, started_at, parent_session_id, ended_at, end_reason, ${extra} FROM sessions WHERE id = ?`);
|
|
787
1668
|
let current = select.get(id);
|
|
788
1669
|
const seen = new Set([id]);
|
|
789
|
-
for (let hop = 0; hop <
|
|
1670
|
+
for (let hop = 0; hop < COMPRESSION_CHAIN_CAP && current; hop += 1) {
|
|
790
1671
|
const parentId = str(current.parent_session_id);
|
|
791
1672
|
if (!parentId || seen.has(parentId))
|
|
792
1673
|
break;
|
|
@@ -800,9 +1681,13 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
|
|
|
800
1681
|
}
|
|
801
1682
|
}
|
|
802
1683
|
const activeClause = available.has('active') ? ' AND (active IS NULL OR active != 0)' : '';
|
|
1684
|
+
if (opts.afterId !== undefined && !hasId)
|
|
1685
|
+
return { ...none, ok: true };
|
|
1686
|
+
const range = opts.afterId === undefined ? '' : ' AND id > ?';
|
|
1687
|
+
const rangeArgs = opts.afterId === undefined ? [] : [opts.afterId];
|
|
803
1688
|
const order = hasId ? 'id' : 'timestamp';
|
|
804
|
-
const rows = db.prepare(`SELECT ${selected.join(', ')}, session_id FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')})${activeClause} ORDER BY ${order} ASC`).all(...chain);
|
|
805
|
-
|
|
1689
|
+
const rows = db.prepare(`SELECT ${selected.join(', ')}, session_id FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')})${activeClause}${range} ORDER BY ${order} ASC`).all(...chain, ...rangeArgs);
|
|
1690
|
+
let projected = rows.map((row) => projectStateDbMessage(row, hasId));
|
|
806
1691
|
// TAL-709: an in-place compaction archives the live rows and re-inserts each as a copy that keeps its role and timestamp
|
|
807
1692
|
// (content may be pruned). A copy takes the id of the oldest archived row it copies, so the merge sees a row it has
|
|
808
1693
|
// already read instead of a new one. Only a compaction archive (`compacted = 1`) vouches for a changed copy: a rewound
|
|
@@ -813,7 +1698,7 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
|
|
|
813
1698
|
// compactions it holds.
|
|
814
1699
|
const sameCall = available.has('tool_call_id') ? " AND COALESCE(a.tool_call_id, '') = COALESCE(m.tool_call_id, '')" : '';
|
|
815
1700
|
const vouched = [available.has('compacted') ? 'a.compacted = 1' : '', available.has('tool_call_id') ? 'm.tool_call_id IS NOT NULL' : '', 'a.content = m.content'].filter(Boolean).join(' OR ');
|
|
816
|
-
const twins = db.prepare(`SELECT m.id AS id, MIN(a.id) AS twin FROM messages m JOIN messages a ON a.session_id = m.session_id AND a.active = 0 AND a.role = m.role AND a.timestamp = m.timestamp AND a.id < m.id${sameCall} WHERE m.session_id IN (${chain.map(() => '?').join(', ')}) AND (m.active IS NULL OR m.active != 0) AND (${vouched}) GROUP BY m.id`).all(...chain);
|
|
1701
|
+
const twins = db.prepare(`SELECT m.id AS id, MIN(a.id) AS twin FROM messages m JOIN messages a ON a.session_id = m.session_id AND a.active = 0 AND a.role = m.role AND a.timestamp = m.timestamp AND a.id < m.id${sameCall} WHERE m.session_id IN (${chain.map(() => '?').join(', ')}) AND (m.active IS NULL OR m.active != 0) AND (${vouched})${opts.afterId === undefined ? '' : ' AND m.id > ?'} GROUP BY m.id`).all(...chain, ...rangeArgs);
|
|
817
1702
|
const twinOf = new Map(twins.map((row) => [row.id, row.twin]));
|
|
818
1703
|
for (const [i, row] of rows.entries()) {
|
|
819
1704
|
const twin = twinOf.get(row.id);
|
|
@@ -821,25 +1706,66 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
|
|
|
821
1706
|
projected[i]._state_db_row_id = twin;
|
|
822
1707
|
}
|
|
823
1708
|
}
|
|
1709
|
+
let identities;
|
|
1710
|
+
if (hasId && ['message_uid', 'active', 'compacted'].every((c) => available.has(c))) {
|
|
1711
|
+
identities = replayIdentities(db, local?.context ?? chain, rows, available);
|
|
1712
|
+
for (const [i, row] of rows.entries()) {
|
|
1713
|
+
const identity = identities.get(Number(row.id));
|
|
1714
|
+
// A known changed generation overrides even a coincidental legacy timestamp twin.
|
|
1715
|
+
if (identity !== undefined) {
|
|
1716
|
+
projected[i]._state_db_row_id = identity.id;
|
|
1717
|
+
if (identity.id !== row.id)
|
|
1718
|
+
projected[i]._state_db_source_key = identity.sourceKey;
|
|
1719
|
+
if (identity.changed)
|
|
1720
|
+
projected[i]._state_db_authored_change = true;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
// The kept reply and summary have independent archive identities; neither can consume the other's copy.
|
|
1725
|
+
const withCarrierSummary = (row, message) => {
|
|
1726
|
+
const carrier = compactionCarrier(row);
|
|
1727
|
+
return carrier ? [message, { role: 'assistant', content: carrier.summary, timestamp: row.timestamp,
|
|
1728
|
+
_context_compression_marker: true, ...(hasId ? { _state_db_row_id: identities?.get(Number(row.id))?.summaryId ?? row.id } : {}) }] : [message];
|
|
1729
|
+
};
|
|
1730
|
+
projected = rows.flatMap((row, i) => withCarrierSummary(row, projected[i]));
|
|
824
1731
|
// TAL-493: the database file's identity, so a marker taken in a replaced state.db is never applied to its successor.
|
|
1732
|
+
const generation = stateDbGeneration(dbPath);
|
|
825
1733
|
if (hasId) {
|
|
826
|
-
const generation = stateDbGeneration(dbPath);
|
|
827
1734
|
for (const m of projected)
|
|
828
1735
|
m._state_db_generation = generation;
|
|
829
1736
|
}
|
|
830
|
-
|
|
1737
|
+
let anchor;
|
|
1738
|
+
if (opts.afterId !== undefined) {
|
|
1739
|
+
anchor = projected.find((m) => m._state_db_row_id === opts.afterId);
|
|
1740
|
+
if (!anchor) {
|
|
1741
|
+
const row = db.prepare(`SELECT ${selected.join(', ')}, session_id FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')}) AND id = ?${activeClause} LIMIT 1`).get(...chain, opts.afterId);
|
|
1742
|
+
if (row)
|
|
1743
|
+
anchor = { ...withCarrierSummary(row, projectStateDbMessage(row, true)).at(-1), _state_db_generation: generation };
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
let requiresFullRead = false;
|
|
1747
|
+
if (opts.afterId !== undefined && opts.unidentified) {
|
|
1748
|
+
const matches = projected.filter((m) => opts.unidentified.some((keys) => keys.has(stateDbMessageKey(m))));
|
|
1749
|
+
if (matches.length) {
|
|
1750
|
+
// Projection can normalize content/timestamps. A bounded role-key superset is conservative copy detection.
|
|
1751
|
+
const roles = [...new Set(matches.map((m) => m.role))];
|
|
1752
|
+
const covered = db.prepare(`SELECT 1 FROM messages WHERE session_id IN (${chain.map(() => '?').join(', ')}) AND id <= ? AND role IN (SELECT value FROM json_each(?))${activeClause} LIMIT 1`).get(...chain, opts.afterId, JSON.stringify(roles));
|
|
1753
|
+
requiresFullRead = Boolean(covered) || available.has('message_uid');
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
return { rows: projected, idCapable: hasId, ok: true, reason: null, generation, requiresFullRead, ...(anchor ? { anchor } : {}) };
|
|
831
1757
|
}
|
|
832
|
-
catch {
|
|
833
|
-
return none;
|
|
1758
|
+
catch (error) {
|
|
1759
|
+
return { ...none, reason: stateDbReadFailure(error).reason };
|
|
834
1760
|
}
|
|
835
1761
|
finally {
|
|
836
|
-
db
|
|
1762
|
+
finishStateDbRead(db);
|
|
837
1763
|
}
|
|
838
1764
|
}
|
|
839
1765
|
/**
|
|
840
1766
|
* TAL-529: `sid` and each compression continuation the Agent rotated it to, down to the live tip (Python
|
|
841
|
-
* `resolve_live_compression_tip`). The walk stops at the last unambiguous row: no continuation, several, a cycle, or
|
|
842
|
-
* hops. A missing or unreadable database is `[sid]`.
|
|
1767
|
+
* `resolve_live_compression_tip`). The walk stops at the last unambiguous row: no continuation, several, a cycle, or
|
|
1768
|
+
* `COMPRESSION_CHAIN_CAP` hops. A missing or unreadable database is `[sid]`.
|
|
843
1769
|
*/
|
|
844
1770
|
export function stateDbCompressionLineage(dbPath, sid) {
|
|
845
1771
|
const lineage = [sid.trim()];
|
|
@@ -853,14 +1779,18 @@ export function stateDbCompressionLineage(dbPath, sid) {
|
|
|
853
1779
|
return lineage;
|
|
854
1780
|
}
|
|
855
1781
|
try {
|
|
1782
|
+
const local = localLineages(db, lineage[0]).get(lineage[0]);
|
|
1783
|
+
if (local)
|
|
1784
|
+
return [...local.ids];
|
|
856
1785
|
const sessionCols = tableColumns(db, 'sessions');
|
|
857
1786
|
if (!['id', 'parent_session_id', 'end_reason', 'ended_at', 'started_at', 'source'].every((c) => sessionCols.has(c)))
|
|
858
1787
|
return lineage;
|
|
859
1788
|
const sessionSource = sessionCols.has('session_source') ? 'session_source' : 'NULL AS session_source';
|
|
860
|
-
const
|
|
1789
|
+
const modelConfig = sessionCols.has('model_config') ? 'model_config' : 'NULL AS model_config';
|
|
1790
|
+
const cols = `id, source, ${sessionSource}, started_at, parent_session_id, ended_at, end_reason, ${modelConfig}`;
|
|
861
1791
|
let current = db.prepare(`SELECT ${cols} FROM sessions WHERE id = ?`).get(lineage[0]);
|
|
862
1792
|
const children = db.prepare(`SELECT ${cols} FROM sessions WHERE parent_session_id = ?`);
|
|
863
|
-
for (let hop = 0; hop <
|
|
1793
|
+
for (let hop = 0; hop < COMPRESSION_CHAIN_CAP && current; hop += 1) {
|
|
864
1794
|
const parent = current;
|
|
865
1795
|
const next = children.all(str(parent.id)).filter((child) => isContinuationSession(parent, child));
|
|
866
1796
|
if (next.length !== 1 || lineage.includes(str(next[0].id)))
|
|
@@ -874,7 +1804,7 @@ export function stateDbCompressionLineage(dbPath, sid) {
|
|
|
874
1804
|
return lineage.slice(0, 1);
|
|
875
1805
|
}
|
|
876
1806
|
finally {
|
|
877
|
-
db
|
|
1807
|
+
finishStateDbRead(db);
|
|
878
1808
|
}
|
|
879
1809
|
}
|
|
880
1810
|
function lineageReportRow(row, role) {
|
|
@@ -884,7 +1814,7 @@ function lineageReportRow(row, role) {
|
|
|
884
1814
|
end_reason: row.end_reason ?? null, active: ended === null, archived: false,
|
|
885
1815
|
};
|
|
886
1816
|
}
|
|
887
|
-
const emptyLineageReport = (sid) => ({
|
|
1817
|
+
export const emptyLineageReport = (sid) => ({
|
|
888
1818
|
mutation: false, found: false, session_id: sid, lineage_key: sid, tip_session_id: sid, total_segments: 0, materialized_segments: 0, segments: [], children: [], manual_review: false,
|
|
889
1819
|
});
|
|
890
1820
|
/**
|
|
@@ -893,7 +1823,7 @@ const emptyLineageReport = (sid) => ({
|
|
|
893
1823
|
* newest first. A cycle, a hop limit hit, or a continuation off the walked path marks `manual_review`. A missing,
|
|
894
1824
|
* unreadable, or too-old database, or an unknown id, answers `found: false`.
|
|
895
1825
|
*/
|
|
896
|
-
export function stateDbLineageReport(dbPath, rawSid, maxHops =
|
|
1826
|
+
export function stateDbLineageReport(dbPath, rawSid, maxHops = COMPRESSION_CHAIN_CAP) {
|
|
897
1827
|
const sid = rawSid.trim();
|
|
898
1828
|
if (!sid || !existsSync(dbPath))
|
|
899
1829
|
return emptyLineageReport(sid);
|
|
@@ -905,6 +1835,7 @@ export function stateDbLineageReport(dbPath, rawSid, maxHops = 20) {
|
|
|
905
1835
|
return emptyLineageReport(sid);
|
|
906
1836
|
}
|
|
907
1837
|
try {
|
|
1838
|
+
const local = localLineages(db, sid).get(sid);
|
|
908
1839
|
const cols = tableColumns(db, 'sessions');
|
|
909
1840
|
if (!['id', 'parent_session_id', 'end_reason'].every((c) => cols.has(c)))
|
|
910
1841
|
return emptyLineageReport(sid);
|
|
@@ -915,10 +1846,10 @@ export function stateDbLineageReport(dbPath, rawSid, maxHops = 20) {
|
|
|
915
1846
|
const target = fetch(sid);
|
|
916
1847
|
if (!target)
|
|
917
1848
|
return emptyLineageReport(sid);
|
|
918
|
-
const segments = [target];
|
|
919
|
-
const seen = new Set(
|
|
920
|
-
let manualReview = true;
|
|
921
|
-
for (let hop = 0; hop < maxHops; hop += 1) {
|
|
1849
|
+
const segments = local ? [...local.rows].reverse().slice(0, Math.max(1, maxHops)) : [target];
|
|
1850
|
+
const seen = new Set(segments.map((row) => str(row.id)));
|
|
1851
|
+
let manualReview = local ? local.ids.length > maxHops : true;
|
|
1852
|
+
for (let hop = 0; !local && hop < maxHops; hop += 1) {
|
|
922
1853
|
const current = segments[segments.length - 1];
|
|
923
1854
|
const parentId = str(current.parent_session_id);
|
|
924
1855
|
const parent = fetch(parentId);
|
|
@@ -959,7 +1890,7 @@ export function stateDbLineageReport(dbPath, rawSid, maxHops = 20) {
|
|
|
959
1890
|
return emptyLineageReport(sid);
|
|
960
1891
|
}
|
|
961
1892
|
finally {
|
|
962
|
-
db
|
|
1893
|
+
finishStateDbRead(db);
|
|
963
1894
|
}
|
|
964
1895
|
}
|
|
965
1896
|
/** The `sessions` row for `sid` (source and lifecycle columns), or null. */
|
|
@@ -985,7 +1916,7 @@ export function stateDbSessionRow(dbPath, sid) {
|
|
|
985
1916
|
return null;
|
|
986
1917
|
}
|
|
987
1918
|
finally {
|
|
988
|
-
db
|
|
1919
|
+
finishStateDbRead(db);
|
|
989
1920
|
}
|
|
990
1921
|
}
|
|
991
1922
|
/**
|
|
@@ -1011,21 +1942,31 @@ export function insightsSessionRows(dbPath, cutoff) {
|
|
|
1011
1942
|
const col = (name) => (cols.has(name) ? `s.${name}` : 'NULL');
|
|
1012
1943
|
const messageCols = tableColumns(db, 'messages');
|
|
1013
1944
|
const hasMessages = messageCols.has('session_id') && messageCols.has('timestamp');
|
|
1014
|
-
|
|
1015
|
-
try {
|
|
1016
|
-
indexed = hasMessages && db.prepare('PRAGMA index_list(messages)').all().some((r) => r.name === 'idx_messages_session');
|
|
1017
|
-
}
|
|
1018
|
-
catch {
|
|
1019
|
-
indexed = false;
|
|
1020
|
-
}
|
|
1945
|
+
const indexed = hasMessages && tableIndexes(db, 'messages').has('idx_messages_session');
|
|
1021
1946
|
// Without the index a per-session lookup scans messages once per session; one grouped scan replaces it.
|
|
1022
|
-
const lastMessage = !hasMessages ? 'NULL' : indexed ?
|
|
1023
|
-
const
|
|
1947
|
+
const lastMessage = !hasMessages ? 'NULL' : indexed ? lastMessageTimestamp(indexed) : 'lm.last_message_at';
|
|
1948
|
+
const messageCte = hasMessages && !indexed ? 'latest_messages AS MATERIALIZED (SELECT session_id, MAX(timestamp) AS last_message_at FROM messages GROUP BY session_id)' : '';
|
|
1949
|
+
const join = messageCte ? 'LEFT JOIN latest_messages lm ON lm.session_id = s.id' : '';
|
|
1024
1950
|
const source = cols.has('source') ? "LOWER(COALESCE(s.source, '')) != 'webui'" : '1';
|
|
1025
|
-
const
|
|
1951
|
+
const activityIndexed = cols.has('last_activity_at') && tableIndexes(db, 'sessions').has('idx_sessions_effective_activity');
|
|
1952
|
+
const activity = activityIndexed ? freshestActivity('s.last_activity_at', `MAX(COALESCE(${col('ended_at')}, 0), COALESCE(${lastMessage}, 0))`)
|
|
1953
|
+
: `MAX(COALESCE(s.started_at, 0), COALESCE(${col('ended_at')}, 0), COALESCE(${lastMessage}, 0))`;
|
|
1954
|
+
// The heartbeat can lag a message or end time; union their candidates before the exact activity check.
|
|
1955
|
+
const candidates = activityIndexed ? [
|
|
1956
|
+
'SELECT s.id FROM sessions s INDEXED BY idx_sessions_effective_activity WHERE COALESCE(s.last_activity_at, s.started_at) >= ?',
|
|
1957
|
+
'SELECT id FROM sessions WHERE started_at >= ?',
|
|
1958
|
+
...(cols.has('ended_at') ? ['SELECT id FROM sessions WHERE ended_at >= ?'] : []),
|
|
1959
|
+
...(hasMessages ? [indexed ? 'SELECT DISTINCT session_id AS id FROM messages WHERE timestamp >= ?' : 'SELECT session_id AS id FROM latest_messages WHERE last_message_at >= ?'] : []),
|
|
1960
|
+
] : [];
|
|
1961
|
+
const ctes = [messageCte, ...(activityIndexed ? [`candidates AS MATERIALIZED (SELECT DISTINCT id FROM (${candidates.join(' UNION ALL ')}))`] : [])].filter(Boolean);
|
|
1962
|
+
const withClause = ctes.length ? `WITH ${ctes.join(', ')} ` : '';
|
|
1963
|
+
// CROSS JOIN keeps the candidate IDs ahead of the expensive per-session freshness projection.
|
|
1964
|
+
const from = activityIndexed ? 'candidates c CROSS JOIN sessions s ON s.id = c.id' : 'sessions s';
|
|
1965
|
+
const ordering = activityIndexed ? ' ORDER BY COALESCE(s.last_activity_at, s.started_at) DESC' : '';
|
|
1966
|
+
const rows = db.prepare(`${withClause}SELECT * FROM (SELECT s.id AS id, ${col('model')} AS model, ${col('message_count')} AS message_count, ${col('input_tokens')} AS input_tokens,
|
|
1026
1967
|
${col('output_tokens')} AS output_tokens, ${col('cache_read_tokens')} AS cache_read_tokens, ${col('estimated_cost_usd')} AS estimated_cost_usd, s.started_at AS started_at,
|
|
1027
|
-
|
|
1028
|
-
FROM
|
|
1968
|
+
${activity} AS last_activity
|
|
1969
|
+
FROM ${from} ${join} WHERE ${source}${ordering}) WHERE last_activity >= ?`).all(...Array(candidates.length + 1).fill(cutoff));
|
|
1029
1970
|
return rows.map((r) => ({
|
|
1030
1971
|
session_id: str(r.id), model: r.model, message_count: r.message_count, input_tokens: r.input_tokens, output_tokens: r.output_tokens,
|
|
1031
1972
|
cache_read_tokens: r.cache_read_tokens, estimated_cost: r.estimated_cost_usd, created_at: r.started_at, updated_at: r.last_activity,
|
|
@@ -1035,7 +1976,7 @@ export function insightsSessionRows(dbPath, cutoff) {
|
|
|
1035
1976
|
return [];
|
|
1036
1977
|
}
|
|
1037
1978
|
finally {
|
|
1038
|
-
db
|
|
1979
|
+
finishStateDbRead(db);
|
|
1039
1980
|
}
|
|
1040
1981
|
}
|
|
1041
1982
|
/** A state.db `timestamp` in unix seconds: a number, or an ISO-8601 string (Python `fromisoformat`); null when unreadable. */
|
|
@@ -1064,7 +2005,7 @@ export function countConversationRounds(dbPath, sid, since = null) {
|
|
|
1064
2005
|
rows = db.prepare('SELECT role, timestamp FROM messages WHERE session_id = ? ORDER BY timestamp ASC').all(sid);
|
|
1065
2006
|
}
|
|
1066
2007
|
finally {
|
|
1067
|
-
db
|
|
2008
|
+
finishStateDbRead(db);
|
|
1068
2009
|
}
|
|
1069
2010
|
}
|
|
1070
2011
|
catch {
|
|
@@ -1105,7 +2046,7 @@ export function stateDbTailToolContent(dbPath, sid) {
|
|
|
1105
2046
|
return typeof content === 'string' ? content : null;
|
|
1106
2047
|
}
|
|
1107
2048
|
finally {
|
|
1108
|
-
db
|
|
2049
|
+
finishStateDbRead(db);
|
|
1109
2050
|
}
|
|
1110
2051
|
}
|
|
1111
2052
|
catch {
|