@maudecode/talaria-web 1.17.2 → 1.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_release.json +8 -8
- package/dist/api/auth-raw.d.ts +1 -1
- package/dist/api/auth-raw.js +20 -20
- package/dist/api/auth-raw.js.map +1 -1
- package/dist/api/automation-raw.d.ts +7 -1
- package/dist/api/automation-raw.js +74 -27
- package/dist/api/automation-raw.js.map +1 -1
- package/dist/api/automation-router.d.ts +128 -23
- package/dist/api/automation-router.js +29 -13
- package/dist/api/automation-router.js.map +1 -1
- package/dist/api/chat-router.d.ts +1845 -40
- package/dist/api/chat-router.js +196 -114
- package/dist/api/chat-router.js.map +1 -1
- package/dist/api/git-router.d.ts +37 -3
- package/dist/api/git-router.js +47 -48
- package/dist/api/git-router.js.map +1 -1
- package/dist/api/raw-routes.d.ts +9 -1
- package/dist/api/raw-routes.js +286 -73
- package/dist/api/raw-routes.js.map +1 -1
- package/dist/api/router.d.ts +16 -6
- package/dist/api/router.js +28 -26
- package/dist/api/router.js.map +1 -1
- package/dist/api/session-visibility.d.ts +4 -4
- package/dist/api/session-visibility.js +26 -16
- package/dist/api/session-visibility.js.map +1 -1
- package/dist/api/sessions-router.d.ts +3312 -131
- package/dist/api/sessions-router.js +287 -157
- package/dist/api/sessions-router.js.map +1 -1
- package/dist/api/settings-router.d.ts +457 -8
- package/dist/api/settings-router.js +54 -33
- package/dist/api/settings-router.js.map +1 -1
- package/dist/api/sse-routes.d.ts +7 -2
- package/dist/api/sse-routes.js +109 -62
- package/dist/api/sse-routes.js.map +1 -1
- package/dist/api/tools-raw.d.ts +5 -0
- package/dist/api/tools-raw.js +63 -55
- package/dist/api/tools-raw.js.map +1 -1
- package/dist/api/tools-router.d.ts +998 -85
- package/dist/api/tools-router.js +447 -61
- package/dist/api/tools-router.js.map +1 -1
- package/dist/api/transcribe-stream.d.ts +16 -0
- package/dist/api/transcribe-stream.js +172 -0
- package/dist/api/transcribe-stream.js.map +1 -0
- package/dist/app.d.ts +4 -1
- package/dist/app.js +98 -59
- package/dist/app.js.map +1 -1
- package/dist/auth/gate.js +15 -11
- package/dist/auth/gate.js.map +1 -1
- package/dist/auth/oidc.d.ts +5 -2
- package/dist/auth/oidc.js +8 -7
- package/dist/auth/oidc.js.map +1 -1
- package/dist/auth/passkeys.d.ts +15 -14
- package/dist/auth/passkeys.js +89 -91
- package/dist/auth/passkeys.js.map +1 -1
- package/dist/auth/store.d.ts +17 -7
- package/dist/auth/store.js +93 -52
- package/dist/auth/store.js.map +1 -1
- package/dist/bin/talaria-web.js +12 -81
- package/dist/bin/talaria-web.js.map +1 -1
- package/dist/cli/ctl.js +2 -1
- package/dist/cli/ctl.js.map +1 -1
- package/dist/cli/dotenv.d.ts +5 -0
- package/dist/cli/dotenv.js +15 -2
- package/dist/cli/dotenv.js.map +1 -1
- package/dist/cli/launcher.js +5 -5
- package/dist/cli/launcher.js.map +1 -1
- package/dist/cli/main.d.ts +1 -0
- package/dist/cli/main.js +85 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/supervise.d.ts +2 -0
- package/dist/cli/supervise.js +67 -4
- package/dist/cli/supervise.js.map +1 -1
- package/dist/config/agent-config.d.ts +34 -11
- package/dist/config/agent-config.js +116 -60
- package/dist/config/agent-config.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.js +19 -6
- package/dist/config.js.map +1 -1
- package/dist/fs/atomic.d.ts +1 -0
- package/dist/fs/atomic.js +4 -1
- package/dist/fs/atomic.js.map +1 -1
- package/dist/fs/stat.d.ts +1 -0
- package/dist/fs/stat.js +11 -0
- package/dist/fs/stat.js.map +1 -0
- package/dist/http/context.d.ts +31 -12
- package/dist/http/context.js +60 -24
- package/dist/http/context.js.map +1 -1
- package/dist/http/csp.d.ts +2 -1
- package/dist/http/csp.js +3 -2
- package/dist/http/csp.js.map +1 -1
- package/dist/onboarding.d.ts +2 -2
- package/dist/onboarding.js +10 -8
- package/dist/onboarding.js.map +1 -1
- package/dist/profiles/profiles.d.ts +20 -4
- package/dist/profiles/profiles.js +115 -30
- package/dist/profiles/profiles.js.map +1 -1
- package/dist/projects.d.ts +15 -6
- package/dist/projects.js +55 -47
- package/dist/projects.js.map +1 -1
- package/dist/providers/catalog.d.ts +84 -14
- package/dist/providers/catalog.js +474 -151
- package/dist/providers/catalog.js.map +1 -1
- package/dist/providers/env-file.d.ts +10 -3
- package/dist/providers/env-file.js +24 -75
- package/dist/providers/env-file.js.map +1 -1
- package/dist/providers/runtime-env.d.ts +4 -2
- package/dist/providers/runtime-env.js +9 -6
- package/dist/providers/runtime-env.js.map +1 -1
- package/dist/providers/tables.d.ts +3 -1
- package/dist/providers/tables.js +4 -2
- package/dist/providers/tables.js.map +1 -1
- package/dist/raster-image.d.ts +7 -0
- package/dist/raster-image.js +267 -0
- package/dist/raster-image.js.map +1 -0
- package/dist/redact.js +342 -46
- package/dist/redact.js.map +1 -1
- package/dist/release.d.ts +7 -0
- package/dist/release.js +14 -0
- package/dist/release.js.map +1 -1
- package/dist/runtime.d.ts +5 -1
- package/dist/runtime.js +305 -96
- package/dist/runtime.js.map +1 -1
- package/dist/server.js +15 -0
- package/dist/server.js.map +1 -1
- package/dist/sessions/anchor.d.ts +9 -3
- package/dist/sessions/anchor.js +28 -36
- package/dist/sessions/anchor.js.map +1 -1
- package/dist/sessions/background-events.d.ts +2 -0
- package/dist/sessions/background-events.js +10 -0
- package/dist/sessions/background-events.js.map +1 -0
- package/dist/sessions/background-tasks.d.ts +26 -27
- package/dist/sessions/background-tasks.js +89 -97
- package/dist/sessions/background-tasks.js.map +1 -1
- package/dist/sessions/background-updates.d.ts +15 -8
- package/dist/sessions/background-updates.js +101 -25
- package/dist/sessions/background-updates.js.map +1 -1
- package/dist/sessions/cli-sessions.d.ts +27 -6
- package/dist/sessions/cli-sessions.js +152 -42
- package/dist/sessions/cli-sessions.js.map +1 -1
- package/dist/sessions/completions.d.ts +77 -9
- package/dist/sessions/completions.js +482 -45
- package/dist/sessions/completions.js.map +1 -1
- package/dist/sessions/compress.d.ts +8 -0
- package/dist/sessions/compress.js +2 -2
- package/dist/sessions/compress.js.map +1 -1
- package/dist/sessions/drafts.d.ts +14 -10
- package/dist/sessions/drafts.js +38 -43
- package/dist/sessions/drafts.js.map +1 -1
- package/dist/sessions/export.d.ts +5 -0
- package/dist/sessions/export.js +30 -0
- package/dist/sessions/export.js.map +1 -1
- package/dist/sessions/gateway-sessions.d.ts +46 -0
- package/dist/sessions/gateway-sessions.js +378 -0
- package/dist/sessions/gateway-sessions.js.map +1 -0
- package/dist/sessions/gateway-watcher.d.ts +7 -3
- package/dist/sessions/gateway-watcher.js +31 -19
- package/dist/sessions/gateway-watcher.js.map +1 -1
- package/dist/sessions/goals.d.ts +31 -0
- package/dist/sessions/goals.js +75 -0
- package/dist/sessions/goals.js.map +1 -0
- package/dist/sessions/handoff.d.ts +32 -0
- package/dist/sessions/handoff.js +114 -0
- package/dist/sessions/handoff.js.map +1 -0
- package/dist/sessions/journal.d.ts +5 -79
- package/dist/sessions/journal.js +15 -606
- package/dist/sessions/journal.js.map +1 -1
- package/dist/sessions/list.d.ts +31 -16
- package/dist/sessions/list.js +139 -129
- package/dist/sessions/list.js.map +1 -1
- package/dist/sessions/merge.d.ts +69 -13
- package/dist/sessions/merge.js +265 -64
- package/dist/sessions/merge.js.map +1 -1
- package/dist/sessions/notices.d.ts +22 -0
- package/dist/sessions/notices.js +72 -0
- package/dist/sessions/notices.js.map +1 -0
- package/dist/sessions/pending.d.ts +10 -4
- package/dist/sessions/pending.js +83 -7
- package/dist/sessions/pending.js.map +1 -1
- package/dist/sessions/prefix-index.d.ts +82 -0
- package/dist/sessions/prefix-index.js +232 -0
- package/dist/sessions/prefix-index.js.map +1 -0
- package/dist/sessions/recovery.d.ts +19 -0
- package/dist/sessions/recovery.js +322 -0
- package/dist/sessions/recovery.js.map +1 -0
- package/dist/sessions/relay.d.ts +38 -19
- package/dist/sessions/relay.js +302 -79
- package/dist/sessions/relay.js.map +1 -1
- package/dist/sessions/service.d.ts +260 -55
- package/dist/sessions/service.js +1448 -462
- package/dist/sessions/service.js.map +1 -1
- package/dist/sessions/session.d.ts +24 -2
- package/dist/sessions/session.js +69 -7
- package/dist/sessions/session.js.map +1 -1
- package/dist/sessions/shares.d.ts +7 -6
- package/dist/sessions/shares.js +22 -45
- package/dist/sessions/shares.js.map +1 -1
- package/dist/sessions/source-kind.js +1 -0
- package/dist/sessions/source-kind.js.map +1 -1
- package/dist/sessions/state-db-identity.d.ts +18 -0
- package/dist/sessions/state-db-identity.js +67 -0
- package/dist/sessions/state-db-identity.js.map +1 -0
- package/dist/sessions/state-db-worker.d.ts +1 -0
- package/dist/sessions/state-db-worker.js +35 -0
- package/dist/sessions/state-db-worker.js.map +1 -0
- package/dist/sessions/state-db.d.ts +113 -7
- package/dist/sessions/state-db.js +861 -44
- package/dist/sessions/state-db.js.map +1 -1
- package/dist/sessions/storage/backend.d.ts +162 -0
- package/dist/sessions/storage/backend.js +19 -0
- package/dist/sessions/storage/backend.js.map +1 -0
- package/dist/sessions/storage/conformance.d.ts +20 -0
- package/dist/sessions/storage/conformance.js +237 -0
- package/dist/sessions/storage/conformance.js.map +1 -0
- package/dist/sessions/storage/document.d.ts +16 -0
- package/dist/sessions/storage/document.js +44 -0
- package/dist/sessions/storage/document.js.map +1 -0
- package/dist/sessions/storage/json-documents.d.ts +23 -0
- package/dist/sessions/storage/json-documents.js +233 -0
- package/dist/sessions/storage/json-documents.js.map +1 -0
- package/dist/sessions/storage/json-files.d.ts +12 -0
- package/dist/sessions/storage/json-files.js +124 -0
- package/dist/sessions/storage/json-files.js.map +1 -0
- package/dist/sessions/storage/json-installation.d.ts +3 -0
- package/dist/sessions/storage/json-installation.js +31 -0
- package/dist/sessions/storage/json-installation.js.map +1 -0
- package/dist/sessions/storage/json-journal.d.ts +114 -0
- package/dist/sessions/storage/json-journal.js +686 -0
- package/dist/sessions/storage/json-journal.js.map +1 -0
- package/dist/sessions/storage/json-recovery.d.ts +4 -0
- package/dist/sessions/storage/json-recovery.js +195 -0
- package/dist/sessions/storage/json-recovery.js.map +1 -0
- package/dist/sessions/storage/json.d.ts +68 -0
- package/dist/sessions/storage/json.js +258 -0
- package/dist/sessions/storage/json.js.map +1 -0
- package/dist/sessions/storage/row.d.ts +6 -0
- package/dist/sessions/storage/row.js +49 -0
- package/dist/sessions/storage/row.js.map +1 -0
- package/dist/sessions/storage/search-text.d.ts +4 -0
- package/dist/sessions/storage/search-text.js +16 -0
- package/dist/sessions/storage/search-text.js.map +1 -0
- package/dist/sessions/store.d.ts +65 -68
- package/dist/sessions/store.js +305 -602
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/streams.d.ts +4 -0
- package/dist/sessions/streams.js +9 -1
- package/dist/sessions/streams.js.map +1 -1
- package/dist/sessions/titles.d.ts +2 -0
- package/dist/sessions/titles.js +5 -1
- package/dist/sessions/titles.js.map +1 -1
- package/dist/sessions/todo.d.ts +5 -0
- package/dist/sessions/todo.js +28 -21
- package/dist/sessions/todo.js.map +1 -1
- package/dist/sessions/tool-display.d.ts +5 -4
- package/dist/sessions/tool-display.js +30 -50
- package/dist/sessions/tool-display.js.map +1 -1
- package/dist/sessions/turn-context.d.ts +11 -2
- package/dist/sessions/turn-context.js +18 -11
- package/dist/sessions/turn-context.js.map +1 -1
- package/dist/sessions/turn.d.ts +122 -17
- package/dist/sessions/turn.js +939 -358
- package/dist/sessions/turn.js.map +1 -1
- package/dist/sessions/wakeup-pause.d.ts +54 -0
- package/dist/sessions/wakeup-pause.js +163 -0
- package/dist/sessions/wakeup-pause.js.map +1 -0
- package/dist/sessions/window.js +13 -12
- package/dist/sessions/window.js.map +1 -1
- package/dist/settings.d.ts +19 -7
- package/dist/settings.js +178 -184
- package/dist/settings.js.map +1 -1
- package/dist/sidecar/client.d.ts +17 -3
- package/dist/sidecar/client.js +78 -8
- package/dist/sidecar/client.js.map +1 -1
- package/dist/sidecar/discover.d.ts +2 -1
- package/dist/sidecar/discover.js +93 -10
- package/dist/sidecar/discover.js.map +1 -1
- package/dist/sidecar/env.d.ts +4 -0
- package/dist/sidecar/env.js +3 -0
- package/dist/sidecar/env.js.map +1 -0
- package/dist/sidecar/fake.d.ts +2 -2
- package/dist/sidecar/fake.js +19 -2
- package/dist/sidecar/fake.js.map +1 -1
- package/dist/startup.d.ts +13 -5
- package/dist/startup.js +48 -0
- package/dist/startup.js.map +1 -1
- package/dist/test/env-file.d.ts +5 -0
- package/dist/test/env-file.js +88 -0
- package/dist/test/env-file.js.map +1 -0
- package/dist/test/harness.d.ts +1 -0
- package/dist/test/harness.js +1 -1
- package/dist/test/harness.js.map +1 -1
- package/dist/test/passkeys.d.ts +22 -0
- package/dist/test/passkeys.js +62 -0
- package/dist/test/passkeys.js.map +1 -0
- package/dist/test/storage.d.ts +15 -0
- package/dist/test/storage.js +26 -0
- package/dist/test/storage.js.map +1 -0
- package/dist/test/ts-worker-hooks.d.ts +1 -0
- package/dist/test/ts-worker-hooks.js +24 -0
- package/dist/test/ts-worker-hooks.js.map +1 -0
- package/dist/text/diff.d.ts +9 -14
- package/dist/text/diff.js +74 -108
- package/dist/text/diff.js.map +1 -1
- package/dist/tools/commands.d.ts +6 -1
- package/dist/tools/commands.js +8 -2
- package/dist/tools/commands.js.map +1 -1
- package/dist/tools/connectors.d.ts +33 -0
- package/dist/tools/connectors.js +121 -0
- package/dist/tools/connectors.js.map +1 -0
- package/dist/tools/crons.d.ts +38 -1
- package/dist/tools/crons.js +122 -18
- package/dist/tools/crons.js.map +1 -1
- package/dist/tools/dashboard-plugins.d.ts +53 -0
- package/dist/tools/dashboard-plugins.js +188 -0
- package/dist/tools/dashboard-plugins.js.map +1 -0
- package/dist/tools/extensions.d.ts +18 -5
- package/dist/tools/extensions.js +344 -266
- package/dist/tools/extensions.js.map +1 -1
- package/dist/tools/gateway-restart.d.ts +62 -0
- package/dist/tools/gateway-restart.js +142 -0
- package/dist/tools/gateway-restart.js.map +1 -0
- package/dist/tools/health.d.ts +44 -0
- package/dist/tools/health.js +201 -46
- package/dist/tools/health.js.map +1 -1
- package/dist/tools/hygiene.d.ts +4 -3
- package/dist/tools/hygiene.js +23 -9
- package/dist/tools/hygiene.js.map +1 -1
- package/dist/tools/kanban.d.ts +4 -2
- package/dist/tools/kanban.js +28 -19
- package/dist/tools/kanban.js.map +1 -1
- package/dist/tools/mcp.d.ts +13 -0
- package/dist/tools/mcp.js +88 -0
- package/dist/tools/mcp.js.map +1 -1
- package/dist/tools/memory-masks.d.ts +5 -0
- package/dist/tools/memory-masks.js +256 -0
- package/dist/tools/memory-masks.js.map +1 -0
- package/dist/tools/memory.d.ts +29 -3
- package/dist/tools/memory.js +69 -27
- package/dist/tools/memory.js.map +1 -1
- package/dist/tools/plugin-settings.d.ts +15 -0
- package/dist/tools/plugin-settings.js +90 -0
- package/dist/tools/plugin-settings.js.map +1 -0
- package/dist/tools/project-os.d.ts +11 -0
- package/dist/tools/project-os.js +267 -0
- package/dist/tools/project-os.js.map +1 -0
- package/dist/tools/skills.d.ts +14 -5
- package/dist/tools/skills.js +49 -16
- package/dist/tools/skills.js.map +1 -1
- package/dist/tools/update-notifications.d.ts +34 -15
- package/dist/tools/update-notifications.js +141 -57
- package/dist/tools/update-notifications.js.map +1 -1
- package/dist/tools/update-rollback.d.ts +11 -0
- package/dist/tools/update-rollback.js +28 -0
- package/dist/tools/update-rollback.js.map +1 -0
- package/dist/tools/updates.d.ts +110 -32
- package/dist/tools/updates.js +1112 -294
- package/dist/tools/updates.js.map +1 -1
- package/dist/tools/wiki.d.ts +68 -0
- package/dist/tools/wiki.js +413 -0
- package/dist/tools/wiki.js.map +1 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.js +5 -0
- package/dist/util.js.map +1 -1
- package/dist/workspace/escape.d.ts +61 -0
- package/dist/workspace/escape.js +149 -0
- package/dist/workspace/escape.js.map +1 -0
- package/dist/workspace/extract.d.ts +22 -0
- package/dist/workspace/extract.js +308 -0
- package/dist/workspace/extract.js.map +1 -0
- package/dist/workspace/fs.d.ts +12 -0
- package/dist/workspace/fs.js +92 -23
- package/dist/workspace/fs.js.map +1 -1
- package/dist/workspace/git.d.ts +34 -1
- package/dist/workspace/git.js +328 -35
- package/dist/workspace/git.js.map +1 -1
- package/dist/workspace/media.d.ts +16 -13
- package/dist/workspace/media.js +43 -61
- package/dist/workspace/media.js.map +1 -1
- package/dist/workspace/rollback.d.ts +36 -22
- package/dist/workspace/rollback.js +178 -272
- package/dist/workspace/rollback.js.map +1 -1
- package/dist/workspace/unzip.js +4 -1
- package/dist/workspace/unzip.js.map +1 -1
- package/dist/workspace/upload.d.ts +7 -4
- package/dist/workspace/upload.js +5 -2
- package/dist/workspace/upload.js.map +1 -1
- package/dist/workspace/workspaces.d.ts +15 -19
- package/dist/workspace/workspaces.js +55 -127
- package/dist/workspace/workspaces.js.map +1 -1
- package/dist/workspace/worktrees.d.ts +2 -1
- package/dist/workspace/worktrees.js +179 -42
- package/dist/workspace/worktrees.js.map +1 -1
- package/package.json +7 -3
- package/sidecar/agent_dependency.json +4 -4
- package/sidecar/agent_support.json +26 -0
- package/sidecar/scripts/record-fixtures.py +12 -3
- package/sidecar/scripts/replay_sidecar.py +32 -0
- package/sidecar/scripts/suite_runner.py +302 -0
- package/sidecar/scripts/test.sh +12 -4
- package/sidecar/talaria_sidecar/__init__.py +6 -1
- package/sidecar/talaria_sidecar/__main__.py +4 -2
- package/sidecar/talaria_sidecar/agent_mutation.py +975 -0
- package/sidecar/talaria_sidecar/agent_services.py +1053 -0
- package/sidecar/talaria_sidecar/dependency_sync.py +85 -0
- package/sidecar/talaria_sidecar/home.py +56 -3
- package/sidecar/talaria_sidecar/methods/__init__.py +2 -2
- package/sidecar/talaria_sidecar/methods/aux.py +166 -24
- package/sidecar/talaria_sidecar/methods/chat.py +852 -135
- package/sidecar/talaria_sidecar/methods/checkpoints.py +89 -0
- package/sidecar/talaria_sidecar/methods/commands.py +294 -4
- package/sidecar/talaria_sidecar/methods/config.py +123 -4
- package/sidecar/talaria_sidecar/methods/connectors.py +343 -0
- package/sidecar/talaria_sidecar/methods/cron.py +395 -36
- package/sidecar/talaria_sidecar/methods/cron_execution.py +284 -0
- package/sidecar/talaria_sidecar/methods/gateway.py +212 -55
- package/sidecar/talaria_sidecar/methods/gateway_sessions.py +234 -0
- package/sidecar/talaria_sidecar/methods/goals.py +81 -41
- package/sidecar/talaria_sidecar/methods/kanban.py +108 -34
- package/sidecar/talaria_sidecar/methods/mcp.py +1 -1
- package/sidecar/talaria_sidecar/methods/memory.py +84 -0
- package/sidecar/talaria_sidecar/methods/oauth.py +5 -2
- package/sidecar/talaria_sidecar/methods/process.py +218 -15
- package/sidecar/talaria_sidecar/methods/profiles.py +121 -13
- package/sidecar/talaria_sidecar/methods/providers.py +217 -28
- package/sidecar/talaria_sidecar/methods/runtime.py +69 -1
- package/sidecar/talaria_sidecar/methods/skills.py +67 -4
- package/sidecar/talaria_sidecar/methods/state_db.py +316 -75
- package/sidecar/talaria_sidecar/methods/stt.py +159 -78
- package/sidecar/talaria_sidecar/methods/text.py +14 -1
- package/sidecar/talaria_sidecar/methods/toolsets.py +49 -0
- package/sidecar/talaria_sidecar/methods/usage.py +113 -14
- package/sidecar/talaria_sidecar/methods/worktree.py +26 -19
- package/sidecar/talaria_sidecar/methods/write_approval.py +181 -0
- package/sidecar/talaria_sidecar/rpc.py +45 -4
- package/sidecar/talaria_sidecar/runtime.py +98 -13
- package/sidecar/talaria_sidecar/speed.py +35 -0
- package/static/dist/FILES.txt +126 -126
- package/static/dist/assets/AppShell-cey33v9s.js +8 -0
- package/static/dist/assets/ChatPage-i8wnugr7.js +18 -0
- package/static/dist/assets/ConnectTargets-hg19553x.js +1 -0
- package/static/dist/assets/ExtensionRoute-cly4apm5.js +1 -0
- package/static/dist/assets/Field-j8uqoigz.js +9 -0
- package/static/dist/assets/HubRoute-e4bgz41j.js +2 -0
- package/static/dist/assets/Markdown-lahuichm.js +598 -0
- package/static/dist/assets/RightPanel-lbo4i0fn.js +1 -0
- package/static/dist/assets/Select-gqatm2e0.js +1 -0
- package/static/dist/assets/SessionListPanel-mfqayfjh.js +1 -0
- package/static/dist/assets/SettingsLayout-b8980u7k.js +1 -0
- package/static/dist/assets/States-e7dkgtxf.js +1 -0
- package/static/dist/assets/Toaster-bfqqy786.js +1 -0
- package/static/dist/assets/_app-n1n6pfx1.js +1 -0
- package/static/dist/assets/{_app.ext._extensionId-opyh9y0v.js → _app.ext._extensionId-buxjxav3.js} +1 -1
- package/static/dist/assets/_app.index-jt8zwiu2.js +1 -0
- package/static/dist/assets/_app.insights-cv76ml71.js +1 -0
- package/static/dist/assets/_app.kanban-h6ts53j7.js +1 -0
- package/static/dist/assets/_app.logs-dm873z90.js +1 -0
- package/static/dist/assets/_app.memory-glk5y6m8.js +1 -0
- package/static/dist/assets/_app.profiles-ka6zpstq.js +1 -0
- package/static/dist/assets/{_app.session._sessionId-bfd4h3id.js → _app.session._sessionId-g2vzghvc.js} +1 -1
- package/static/dist/assets/_app.settings-hgjdhzcd.js +1 -0
- package/static/dist/assets/_app.settings._section-jf2kmhae.js +3 -0
- package/static/dist/assets/_app.skills-cii852yq.js +1 -0
- package/static/dist/assets/_app.tasks-fp9q7atm.js +1 -0
- package/static/dist/assets/_app.todos-dw94qzue.js +1 -0
- package/static/dist/assets/_app.workspaces-inyygzze.js +1 -0
- package/static/dist/assets/{abnfDiagram-VCTEODGH-b2x1lqot.js → abnfDiagram-VCTEODGH-gjayq3ra.js} +1 -1
- package/static/dist/assets/architecture-7GRP2DOG-lp56972r.js +1 -0
- package/static/dist/assets/{architectureDiagram-5GKGNRK7-h38o5l7b.js → architectureDiagram-5GKGNRK7-nwb3xovn.js} +1 -1
- package/static/dist/assets/archive-b1j004vn.js +1 -0
- package/static/dist/assets/back-g74gkozu.js +1 -0
- package/static/dist/assets/{blockDiagram-I7D4REHJ-bqquxv2u.js → blockDiagram-I7D4REHJ-joedghi3.js} +1 -1
- package/static/dist/assets/{c4Diagram-7LVT6UL2-f3fb8w6z.js → c4Diagram-7LVT6UL2-hyrjrohe.js} +1 -1
- package/static/dist/assets/channel-es95o4y8.js +1 -0
- package/static/dist/assets/{chunk-4HAMMTFA-bzfyyzp3.js → chunk-4HAMMTFA-koouqbut.js} +1 -1
- package/static/dist/assets/{chunk-75Z2AOVW-ly99rfx5.js → chunk-75Z2AOVW-bl4bhdnt.js} +1 -1
- package/static/dist/assets/{chunk-DU6HZSFF-bg6ecfqf.js → chunk-DU6HZSFF-gpedttsp.js} +3 -3
- package/static/dist/assets/{chunk-F27PBJKO-e8k7mmqw.js → chunk-F27PBJKO-d8xxi0zi.js} +1 -1
- package/static/dist/assets/{chunk-GMAD6QVW-cmalnoaj.js → chunk-GMAD6QVW-jro0hl73.js} +1 -1
- package/static/dist/assets/{chunk-GVQU2GXP-gmif5o01.js → chunk-GVQU2GXP-djn6gbyu.js} +1 -1
- package/static/dist/assets/{chunk-IMKFNOWR-f1nwwh6u.js → chunk-IMKFNOWR-vw4p5p7o.js} +1 -1
- package/static/dist/assets/{chunk-L3NEJ4N5-oktfh5v7.js → chunk-L3NEJ4N5-n1hj77q8.js} +1 -1
- package/static/dist/assets/{chunk-OSK3NFVY-b1sdx96z.js → chunk-OSK3NFVY-oy0gwn2n.js} +1 -1
- package/static/dist/assets/{chunk-P2QGCYS3-fhu4nzxt.js → chunk-P2QGCYS3-onf1hz6c.js} +1 -1
- package/static/dist/assets/{chunk-POPQ4Y6H-em31rnef.js → chunk-POPQ4Y6H-m8sqn7cs.js} +1 -1
- package/static/dist/assets/{chunk-PWAF6VOD-bxhhyftw.js → chunk-PWAF6VOD-ek4smc8x.js} +1 -1
- package/static/dist/assets/{chunk-SHT3W25Y-fmrvwcav.js → chunk-SHT3W25Y-g9gfvefo.js} +3 -3
- package/static/dist/assets/{chunk-SVP7TREG-lymukl2j.js → chunk-SVP7TREG-f9nfv2cu.js} +1 -1
- package/static/dist/assets/{chunk-TICWLB2K-lmza6k23.js → chunk-TICWLB2K-bnk3m1mq.js} +1 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-kmihoqxh.js +1 -0
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-kmihoqxh.js +1 -0
- package/static/dist/assets/copied-bli9tbcx.js +1 -0
- package/static/dist/assets/{copy-fw5b1pc6.js → copy-jamtd1pf.js} +1 -1
- package/static/dist/assets/{cynefin-OW5HDTMX-czlhbyhs.js → cynefin-OW5HDTMX-f8xfdd3f.js} +1 -1
- package/static/dist/assets/{cynefinDiagram-5FMLGOSQ-gq7mvswb.js → cynefinDiagram-5FMLGOSQ-n9lb99h9.js} +1 -1
- package/static/dist/assets/{dagre-GXQ25YYZ-cqdbf7dr.js → dagre-GXQ25YYZ-dgailddi.js} +4 -4
- package/static/dist/assets/{diagram-S7CK7UJ4-o6arxa58.js → diagram-S7CK7UJ4-m6aeoe7c.js} +1 -1
- package/static/dist/assets/{diagram-UQ7AKVKN-nhnd68tz.js → diagram-UQ7AKVKN-jall3j0x.js} +1 -1
- package/static/dist/assets/{diagram-VSXAHHWV-dk2sclzn.js → diagram-VSXAHHWV-hbawi7hn.js} +1 -1
- package/static/dist/assets/{diagram-VX7I27RA-n3k5nyx3.js → diagram-VX7I27RA-hendl2tt.js} +1 -1
- package/static/dist/assets/{diagram-Z3DM3KII-m6q9cwdh.js → diagram-Z3DM3KII-igwc35p3.js} +1 -1
- package/static/dist/assets/{ebnfDiagram-PWID7BFC-lsa5641p.js → ebnfDiagram-PWID7BFC-c64knz08.js} +1 -1
- package/static/dist/assets/{erDiagram-RLTQ6QDP-fo2ckbe6.js → erDiagram-RLTQ6QDP-brdu9lcm.js} +1 -1
- package/static/dist/assets/eventmodeling-NTZA5JFV-njk1j8wl.js +1 -0
- package/static/dist/assets/flowDiagram-HODETNUW-nma6f0ga.js +1 -0
- package/static/dist/assets/folder-plus-lvt9kudb.js +1 -0
- package/static/dist/assets/{ganttDiagram-EL5Y4UJY-nzqz6x9g.js → ganttDiagram-EL5Y4UJY-ma6jxj5i.js} +1 -1
- package/static/dist/assets/{gitGraph-4MIJSDKK-gukvcxe6.js → gitGraph-4MIJSDKK-naa9w6xa.js} +1 -1
- package/static/dist/assets/{gitGraphDiagram-WWUBYQGX-lqhdp8f0.js → gitGraphDiagram-WWUBYQGX-hzltvsod.js} +1 -1
- package/static/dist/assets/{highlighted-body-KPVGNVTW-igw6cp39.js → highlighted-body-KPVGNVTW-ghoavj8d.js} +1 -1
- package/static/dist/assets/host-hzm8n85n.js +1 -0
- package/static/dist/assets/index-drdcaqeq.css +2 -0
- package/static/dist/assets/index-jzm844he.js +11 -0
- package/static/dist/assets/{info-A6RAGUB7-gvu7a93l.js → info-A6RAGUB7-mn2ut5ko.js} +1 -1
- package/static/dist/assets/{infoDiagram-27XIBGKW-m8t8g6w1.js → infoDiagram-27XIBGKW-h6kuqvsb.js} +1 -1
- package/static/dist/assets/{ishikawaDiagram-5VMMS53U-l07vkbgj.js → ishikawaDiagram-5VMMS53U-e7bnwq7h.js} +1 -1
- package/static/dist/assets/{journeyDiagram-3NMN7TZE-hybkgj8l.js → journeyDiagram-3NMN7TZE-etd1bi6p.js} +1 -1
- package/static/dist/assets/{kanban-definition-UXKFOSKX-by13uvtn.js → kanban-definition-UXKFOSKX-emb1g9dz.js} +4 -4
- package/static/dist/assets/lazyRouteComponent-o816ytzl.js +1 -0
- package/static/dist/assets/{line-osxq6fq7.js → line-h0ojzsvs.js} +1 -1
- package/static/dist/assets/link-k01ryaf9.js +1 -0
- package/static/dist/assets/login-bsae49ew.js +1 -0
- package/static/dist/assets/mermaid-HWGCJPDP-ihl34s74.js +1 -0
- package/static/dist/assets/{mermaid-parser.core-i91oxmou.js → mermaid-parser.core-bnre27u8.js} +3 -3
- package/static/dist/assets/{mindmap-definition-YA3MSWOX-m82mx8ww.js → mindmap-definition-YA3MSWOX-o1vathns.js} +1 -1
- package/static/dist/assets/onboarding-c65tysya.js +1 -0
- package/static/dist/assets/onboarding_model_label-gpn28uix.js +1 -0
- package/static/dist/assets/{packet-AYTQ26CC-nbtgge2o.js → packet-AYTQ26CC-hatq7m2e.js} +1 -1
- package/static/dist/assets/pcmTap.worklet-74EKjWAZ.js +1 -0
- package/static/dist/assets/{pegDiagram-XKGWAZYB-f1zxzga1.js → pegDiagram-XKGWAZYB-gwisz8jv.js} +1 -1
- package/static/dist/assets/{pie-WAS4IAKB-of8vuhxs.js → pie-WAS4IAKB-fpftf0mk.js} +1 -1
- package/static/dist/assets/{pieDiagram-E7YTZNPT-o7y8ftio.js → pieDiagram-E7YTZNPT-khm1o9a6.js} +1 -1
- package/static/dist/assets/{quadrantDiagram-AXDQQJYC-iqiwia1t.js → quadrantDiagram-AXDQQJYC-owqc4uqn.js} +1 -1
- package/static/dist/assets/{radar-RG4KPBEZ-delnsy2v.js → radar-RG4KPBEZ-cntfwjpq.js} +1 -1
- package/static/dist/assets/{railroad-74A4TZTK-nspnyq9u.js → railroad-74A4TZTK-a7t5eibp.js} +1 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-ol8cdktf.js +1 -0
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-i8vzd429.js +1 -0
- package/static/dist/assets/railroad-peg-WCYAUIDC-nw3ulewz.js +1 -0
- package/static/dist/assets/{railroadDiagram-O6MQD6OU-z41tqx5z.js → railroadDiagram-O6MQD6OU-etuh0qa0.js} +1 -1
- package/static/dist/assets/{refresh-cw-nt1dtsx4.js → refresh-cw-mqbkv2a3.js} +1 -1
- package/static/dist/assets/refresh-l29ljp76.js +1 -0
- package/static/dist/assets/{requirementDiagram-BXWQKSXE-bdvif194.js → requirementDiagram-BXWQKSXE-oq2ulk22.js} +1 -1
- package/static/dist/assets/{sankeyDiagram-P5KCCOFB-h73e0rol.js → sankeyDiagram-P5KCCOFB-iqc02emv.js} +1 -1
- package/static/dist/assets/saved-ndd4t2xb.js +1 -0
- package/static/dist/assets/{sequenceDiagram-WJ2MYXX4-iv95mb1z.js → sequenceDiagram-WJ2MYXX4-i0v116q0.js} +2 -2
- package/static/dist/assets/settings_section_extensions_title-jhfo6o8z.js +1 -0
- package/static/dist/assets/share._token-c1ri0hy1.js +2 -0
- package/static/dist/assets/sliders-horizontal-j3jjt9tv.js +1 -0
- package/static/dist/assets/{stateDiagram-D77RDMKH-k417ixmf.js → stateDiagram-D77RDMKH-dnpt1ato.js} +1 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-bxjacpk6.js +1 -0
- package/static/dist/assets/{swimlanes-42K2YHIH-esola70w.js → swimlanes-42K2YHIH-loqbdazb.js} +1 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-id62p9hz.js +8 -0
- package/static/dist/assets/{timeline-definition-24CTP7MA-getrz2y8.js → timeline-definition-24CTP7MA-ol2hnvma.js} +1 -1
- package/static/dist/assets/toast-i3h34v6o.js +64 -0
- package/static/dist/assets/{treeView-Q6P3EWNA-hc7ap1q7.js → treeView-Q6P3EWNA-idrj6iox.js} +1 -1
- package/static/dist/assets/{treemap-WGGIJYW6-b7k1syy5.js → treemap-WGGIJYW6-lzr49vv5.js} +1 -1
- package/static/dist/assets/untitled-oaoya4qj.js +1 -0
- package/static/dist/assets/useForm-nfaxgmsk.js +1 -0
- package/static/dist/assets/{useParams-lhg566ew.js → useParams-gjc70qru.js} +1 -1
- package/static/dist/assets/{useSearch-lbrh6ejn.js → useSearch-e6ae539u.js} +1 -1
- package/static/dist/assets/{vennDiagram-4TSXK5OY-n6pcy5qb.js → vennDiagram-4TSXK5OY-bvwr9o2z.js} +1 -1
- package/static/dist/assets/{wardley-WFR3VGLG-lpa18vz1.js → wardley-WFR3VGLG-31ui8tma.js} +1 -1
- package/static/dist/assets/{wardleyDiagram-VM6X3IG4-kpclwwg7.js → wardleyDiagram-VM6X3IG4-k5wqtx4b.js} +1 -1
- package/static/dist/assets/{xychartDiagram-S5SC5T6Z-paa2n8y0.js → xychartDiagram-S5SC5T6Z-aqbgvujg.js} +1 -1
- package/static/dist/index.html +1 -1
- package/static/dist/sw.js +1 -1
- package/static/dist/assets/AppShell-fb2td9zi.js +0 -1
- package/static/dist/assets/ChatPage-cj96kiy9.js +0 -25
- package/static/dist/assets/Dialog-kxcg7ard.js +0 -1
- package/static/dist/assets/ExtensionRoute-jf7h0754.js +0 -1
- package/static/dist/assets/HubRoute-gmanpd5y.js +0 -2
- package/static/dist/assets/Markdown-mdwlvj3w.js +0 -598
- package/static/dist/assets/RightPanel-ftvrjlcp.js +0 -1
- package/static/dist/assets/Select-bilaypej.js +0 -1
- package/static/dist/assets/SessionListPanel-og8n36qs.js +0 -1
- package/static/dist/assets/SettingsLayout-jonjefmj.js +0 -1
- package/static/dist/assets/States-nmejrwrm.js +0 -1
- package/static/dist/assets/Toaster-nnl8zkt9.js +0 -1
- package/static/dist/assets/_app-gdidzd94.js +0 -1
- package/static/dist/assets/_app.index-hbarzbjx.js +0 -1
- package/static/dist/assets/_app.insights-luywlarz.js +0 -1
- package/static/dist/assets/_app.kanban-kincnbr8.js +0 -1
- package/static/dist/assets/_app.logs-oi2gr3gs.js +0 -1
- package/static/dist/assets/_app.memory-b6jfivt6.js +0 -1
- package/static/dist/assets/_app.profiles-lpvixmqp.js +0 -1
- package/static/dist/assets/_app.settings-vowx3iq0.js +0 -1
- package/static/dist/assets/_app.settings._section-cnntg1tu.js +0 -3
- package/static/dist/assets/_app.skills-fb57lpau.js +0 -1
- package/static/dist/assets/_app.tasks-eszpx1q7.js +0 -2
- package/static/dist/assets/_app.todos-nr05ac6w.js +0 -1
- package/static/dist/assets/_app.workspaces-d2luomu6.js +0 -1
- package/static/dist/assets/architecture-7GRP2DOG-hd0o7bde.js +0 -1
- package/static/dist/assets/back-bemaer09.js +0 -1
- package/static/dist/assets/cancel-drd0g6d9.js +0 -1
- package/static/dist/assets/channel-cpdgf867.js +0 -1
- package/static/dist/assets/classDiagram-ZZMXUADV-ko09sguk.js +0 -1
- package/static/dist/assets/classDiagram-v2-VYDZK3BY-ko09sguk.js +0 -1
- package/static/dist/assets/copied-ima3pysb.js +0 -1
- package/static/dist/assets/endpoints-d33siokx.js +0 -64
- package/static/dist/assets/eventmodeling-NTZA5JFV-emctlo64.js +0 -1
- package/static/dist/assets/flowDiagram-HODETNUW-bfi3moqa.js +0 -1
- package/static/dist/assets/index-cxq58xb1.js +0 -11
- package/static/dist/assets/index-hgtx08n5.css +0 -2
- package/static/dist/assets/lazyRouteComponent-af0lvgjx.js +0 -1
- package/static/dist/assets/link-jyxicdhg.js +0 -1
- package/static/dist/assets/login-ec21t39x.js +0 -1
- package/static/dist/assets/mermaid-HWGCJPDP-gfl8z3ep.js +0 -1
- package/static/dist/assets/onboarding-btnltvyz.js +0 -1
- package/static/dist/assets/onboarding_model_label-dp7xffoh.js +0 -1
- package/static/dist/assets/railroad-abnf-HS5TGJTU-nst1qxyn.js +0 -1
- package/static/dist/assets/railroad-ebnf-LZEXJU2U-ci37x8qy.js +0 -1
- package/static/dist/assets/railroad-peg-WCYAUIDC-aimkk07z.js +0 -1
- package/static/dist/assets/refresh-b4uams1i.js +0 -1
- package/static/dist/assets/saved-kg52csy7.js +0 -1
- package/static/dist/assets/settings_section_extensions_title-ej0iapxe.js +0 -1
- package/static/dist/assets/share._token-f97e94r0.js +0 -2
- package/static/dist/assets/sliders-horizontal-fj18it47.js +0 -1
- package/static/dist/assets/sse-fwpqtzbg.js +0 -1
- package/static/dist/assets/stateDiagram-v2-MP3YSRHH-hwn4apct.js +0 -1
- package/static/dist/assets/swimlanesDiagram-VR7AAH4N-d1h97oyg.js +0 -8
- package/static/dist/assets/todoStore-eginf9h4.js +0 -1
- package/static/dist/assets/tts-lxdxnmjd.js +0 -1
- package/static/dist/assets/untitled-ovpfthg4.js +0 -1
- package/static/dist/assets/useForm-bcblv1rx.js +0 -1
- package/static/dist/assets/useLocale-fi5jb8ma.js +0 -1
|
@@ -7,12 +7,19 @@
|
|
|
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';
|
|
11
|
+
import { Worker } from 'node:worker_threads';
|
|
10
12
|
import { str } from '../util.js';
|
|
13
|
+
import { stateDbMessageFingerprint, stateDbMessageKey } from './state-db-identity.js';
|
|
11
14
|
import { MESSAGING_SOURCES } from './source-kind.js';
|
|
12
15
|
import { looksLikeDefaultCliTitle, normalizeSourceName } from './titles.js';
|
|
13
16
|
export const CLI_MIN_UNTITLED_USER_MESSAGE_COUNT = 2;
|
|
14
|
-
|
|
17
|
+
/** The Agent's `_READ_BUSY_TIMEOUT_S` (TAL-890): a DELETE-journal commit by another process blocks readers across its fsyncs. */
|
|
18
|
+
const STATE_DB_BUSY_TIMEOUT_MS = 5000;
|
|
19
|
+
/** The gateway watcher's change check stays cheap; a lock there fails over to a full projection. */
|
|
20
|
+
const STATE_DB_FINGERPRINT_BUSY_TIMEOUT_MS = 500;
|
|
15
21
|
const SOURCE_LABELS = {
|
|
22
|
+
bluebubbles: 'BlueBubbles', dingtalk: 'DingTalk', feishu: 'Feishu', qqbot: 'QQBot', yuanbao: 'Yuanbao', whatsapp_cloud: 'WhatsApp Cloud',
|
|
16
23
|
acp: 'ACP', api_server: 'API', cli: 'CLI', cron: 'Cron', discord: 'Discord', email: 'Email', kanban: 'Kanban', wecom: 'WeCom', wecom_callback: 'WeCom Callback',
|
|
17
24
|
slack: 'Slack', telegram: 'Telegram', tool: 'Tool', tui: 'TUI', webhook: 'Webhook', webui: 'WebUI', weixin: 'Weixin', matrix: 'Matrix', signal: 'Signal',
|
|
18
25
|
};
|
|
@@ -89,6 +96,8 @@ export function isCliSessionRow(row) {
|
|
|
89
96
|
export function isCliSessionRowVisible(row) {
|
|
90
97
|
if (!isCliSessionRow(row))
|
|
91
98
|
return true;
|
|
99
|
+
if (row._lineage_root_id && row._lineage_tip_id)
|
|
100
|
+
return true;
|
|
92
101
|
const actual = positiveInt(row.actual_message_count);
|
|
93
102
|
const messageCount = actual || positiveInt(row.message_count);
|
|
94
103
|
if (messageCount <= 0)
|
|
@@ -107,9 +116,13 @@ export function isCliSessionRowVisible(row) {
|
|
|
107
116
|
return countUserTurns(row) >= CLI_MIN_UNTITLED_USER_MESSAGE_COUNT;
|
|
108
117
|
}
|
|
109
118
|
// ── compression-chain projection ─────────────────────────────────────────
|
|
119
|
+
/** The Agent's `_CHAIN_CAP`: the most hops a walk along a compression lineage takes. Its visited check stops a cycle sooner. */
|
|
120
|
+
const COMPRESSION_CHAIN_CAP = 1000;
|
|
110
121
|
function isContinuationSession(parent, child) {
|
|
111
122
|
if (!parent || !child)
|
|
112
123
|
return false;
|
|
124
|
+
if (lower(child.source) === 'tool' || explicitForkMarkers(child, true).length > 0)
|
|
125
|
+
return false;
|
|
113
126
|
if (lower(child.session_source) === 'fork')
|
|
114
127
|
return false;
|
|
115
128
|
const parentSource = lower(parent.source);
|
|
@@ -202,6 +215,10 @@ export function projectAgentSessionRows(rows) {
|
|
|
202
215
|
};
|
|
203
216
|
const projected = [];
|
|
204
217
|
for (const row of rows) {
|
|
218
|
+
if (row._lineage_root_id && row._lineage_tip_id) {
|
|
219
|
+
projected.push({ ...row });
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
205
222
|
if (continuationChildIds.has(str(row.id)))
|
|
206
223
|
continue;
|
|
207
224
|
let segments = 1;
|
|
@@ -246,15 +263,175 @@ export function openStateDbReadonly(dbPath, busyTimeoutMs = STATE_DB_BUSY_TIMEOU
|
|
|
246
263
|
const db = new DatabaseSync(dbPath, { readOnly: true, timeout: busyTimeoutMs });
|
|
247
264
|
return db;
|
|
248
265
|
}
|
|
266
|
+
/** The table's columns; a failed read throws, so an unreadable database is never mistaken for an older schema (TAL-785). */
|
|
249
267
|
function tableColumns(db, table) {
|
|
268
|
+
return new Set(db.prepare(`PRAGMA table_info(${table})`).all().map((r) => r.name));
|
|
269
|
+
}
|
|
270
|
+
const LOCAL_POLICY_PREFIX = 'gateway.local_policy.v1:';
|
|
271
|
+
/** Read-only equivalent of Agent validate_local_lineage. A receipt, never a reset edge alone, proves ownership. */
|
|
272
|
+
function localLineages(db, sid) {
|
|
273
|
+
const result = new Map();
|
|
274
|
+
if (!tableColumns(db, 'state_meta').has('value'))
|
|
275
|
+
return result;
|
|
276
|
+
const cols = tableColumns(db, 'sessions');
|
|
277
|
+
if (!['chat_id', 'user_id', 'session_key', 'model_config', 'parent_session_id', 'end_reason'].every((c) => cols.has(c)))
|
|
278
|
+
return result;
|
|
279
|
+
const meta = db.prepare('SELECT value FROM state_meta WHERE key = ?');
|
|
280
|
+
const fetch = db.prepare('SELECT * FROM sessions WHERE id IN (SELECT value FROM json_each(?))');
|
|
281
|
+
const messageCols = tableColumns(db, 'messages');
|
|
282
|
+
const lastMessage = ['session_id', 'timestamp'].every((c) => messageCols.has(c)) ? '(SELECT MAX(m.timestamp) FROM messages m WHERE m.session_id = s.id)' : 'NULL';
|
|
283
|
+
const activity = `MAX(COALESCE(${cols.has('last_activity_at') ? 's.last_activity_at' : 'NULL'}, 0), COALESCE(${lastMessage}, 0), COALESCE(s.started_at, 0))`;
|
|
284
|
+
// Agent _CHAIN_STEP_SQL prefers a compression child, then a live child, then recency and id.
|
|
285
|
+
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`);
|
|
286
|
+
const explicitChild = (child, includeReset) => {
|
|
287
|
+
return lower(child.source) === 'tool' || lower(child.session_source) === 'fork'
|
|
288
|
+
|| explicitForkMarkers(child, includeReset).length > 0;
|
|
289
|
+
};
|
|
290
|
+
const compressionChildren = (parent) => str(parent.end_reason) === 'compression'
|
|
291
|
+
? children.all(str(parent.id)).filter((child) => !explicitChild(child, true)) : [];
|
|
292
|
+
const owner = sid ? db.prepare('SELECT chat_id FROM sessions WHERE id = ?').get(sid)?.chat_id || sid : null;
|
|
293
|
+
const receipts = owner
|
|
294
|
+
? db.prepare('SELECT key, value FROM state_meta WHERE key = ?').all(`${LOCAL_POLICY_PREFIX}${str(owner)}`)
|
|
295
|
+
: db.prepare('SELECT key, value FROM state_meta WHERE key GLOB ?').iterate(`${LOCAL_POLICY_PREFIX}*`);
|
|
296
|
+
for (const saved of receipts) {
|
|
297
|
+
try {
|
|
298
|
+
if (saved.value.length > 1_000_000)
|
|
299
|
+
continue;
|
|
300
|
+
const receipt = JSON.parse(saved.value);
|
|
301
|
+
const owner = saved.key.slice(LOCAL_POLICY_PREFIX.length);
|
|
302
|
+
const ids = receipt.lineage;
|
|
303
|
+
const entry = receipt.entry;
|
|
304
|
+
if (receipt.session_id !== owner || !Array.isArray(ids) || ids.length < 2 || ids.length > COMPRESSION_CHAIN_CAP
|
|
305
|
+
|| !ids.every((id) => typeof id === 'string' && Boolean(id)) || new Set(ids).size !== ids.length
|
|
306
|
+
|| ids[0] !== owner || ids.at(-1) !== entry?.session_id)
|
|
307
|
+
continue;
|
|
308
|
+
if (!['profile_id', 'principal_id', 'request_id', 'route'].every((key) => typeof receipt[key] === 'string' && receipt[key]))
|
|
309
|
+
continue;
|
|
310
|
+
if ('legacy_session_id' in receipt) {
|
|
311
|
+
const legacy = meta.get(`gateway.local_legacy.v1:${owner}`);
|
|
312
|
+
if (receipt.legacy_session_id !== owner || !legacy)
|
|
313
|
+
continue;
|
|
314
|
+
const binding = JSON.parse(legacy.value);
|
|
315
|
+
if (!['session_id', 'profile_id', 'principal_id', 'legacy_session_id'].every((key) => binding[key] === receipt[key]))
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
else {
|
|
319
|
+
// Python json.dumps uses ensure_ascii=True, including UTF-16 surrogate pairs.
|
|
320
|
+
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')}`);
|
|
321
|
+
if (owner !== `local-${createHash('sha256').update(identity).digest('hex')}`)
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const byId = new Map(fetch.all(JSON.stringify(ids)).map((row) => [str(row.id), row]));
|
|
325
|
+
const rows = ids.map((id) => byId.get(id));
|
|
326
|
+
if (!rows.every((row) => Boolean(row) && row.chat_id === owner && row.user_id === receipt.principal_id && row.session_key === receipt.route))
|
|
327
|
+
continue;
|
|
328
|
+
let contextStart = 0;
|
|
329
|
+
let valid = true;
|
|
330
|
+
for (let i = 1; i < rows.length; i += 1) {
|
|
331
|
+
const parent = rows[i - 1], child = rows[i];
|
|
332
|
+
if (child.parent_session_id !== parent.id || explicitChild(child, false)) {
|
|
333
|
+
valid = false;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (parent.end_reason === 'session_reset') {
|
|
337
|
+
const config = jsonLoadsIfString(child.model_config);
|
|
338
|
+
if (!config || config._reset_from !== parent.id) {
|
|
339
|
+
valid = false;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
contextStart = i;
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const next = compressionChildren(parent);
|
|
346
|
+
if (next[0]?.id !== child.id) {
|
|
347
|
+
valid = false;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (!valid || compressionChildren(rows.at(-1)).length)
|
|
353
|
+
continue;
|
|
354
|
+
const lineage = { owner, ids, context: ids.slice(contextStart), rows, title: [...rows].reverse().find((row) => row.title)?.title ?? null };
|
|
355
|
+
for (const id of ids)
|
|
356
|
+
result.set(id, lineage);
|
|
357
|
+
}
|
|
358
|
+
catch { /* Malformed receipts cannot attach history to another conversation. */ }
|
|
359
|
+
}
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
/** The validated receipt's current physical target, or the original id on old Agents / unproven metadata. */
|
|
363
|
+
export function stateDbLocalTarget(dbPath, sid, opts = {}) {
|
|
364
|
+
if (!existsSync(dbPath)) {
|
|
365
|
+
opts.onRead?.(sid);
|
|
366
|
+
return sid;
|
|
367
|
+
}
|
|
368
|
+
let db;
|
|
250
369
|
try {
|
|
251
|
-
|
|
370
|
+
db = openStateDbReadonly(dbPath);
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return sid;
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
db.exec('BEGIN');
|
|
377
|
+
const target = localLineages(db, sid).get(sid)?.ids.at(-1) ?? sid;
|
|
378
|
+
opts.onRead?.(target);
|
|
379
|
+
return target;
|
|
252
380
|
}
|
|
253
381
|
catch {
|
|
254
|
-
return
|
|
382
|
+
return sid;
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
db.close();
|
|
255
386
|
}
|
|
256
387
|
}
|
|
388
|
+
export const AGENT_STORAGE_OK = Object.freeze({ state: 'ok', reason: null });
|
|
389
|
+
/**
|
|
390
|
+
* The corrupt-storage reason when `error` is SQLite's `SQLITE_CORRUPT` or `SQLITE_NOTADB` (the Agent's
|
|
391
|
+
* `hermes_state_health`), else null. The message is matched too, since a listing worker failure crosses the thread
|
|
392
|
+
* boundary as its message alone.
|
|
393
|
+
*/
|
|
394
|
+
export function stateDbCorruptReason(error) {
|
|
395
|
+
const code = error?.errcode;
|
|
396
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
397
|
+
const corrupt = (typeof code === 'number' && [11, 26].includes(code & 0xff)) || /malformed|not a database/i.test(message);
|
|
398
|
+
return corrupt ? `Agent session storage (state.db) is damaged: ${message}. Agent sessions are unavailable until it is repaired or restored from a backup.` : null;
|
|
399
|
+
}
|
|
400
|
+
/** The storage state a failed `state.db` read reports: `corrupt` when SQLite says the file is damaged, else `unavailable`. */
|
|
401
|
+
export function stateDbReadFailure(error) {
|
|
402
|
+
const corrupt = stateDbCorruptReason(error);
|
|
403
|
+
if (corrupt)
|
|
404
|
+
return { state: 'corrupt', reason: corrupt };
|
|
405
|
+
return { state: 'unavailable', reason: `Agent session storage (state.db) could not be read: ${error instanceof Error ? error.message : String(error)}.` };
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* TAL-785: whether `state.db` can serve the session list: `corrupt` when SQLite reports the file damaged, `unavailable`
|
|
409
|
+
* when it cannot be read for another reason (permissions, a lock, an I/O error), `outdated` when a readable database
|
|
410
|
+
* has no `sessions.source` column. A missing file is `ok`. Full-text index damage is never probed, so it stays `ok`, as
|
|
411
|
+
* the Agent treats it.
|
|
412
|
+
*/
|
|
413
|
+
export function stateDbStorage(dbPath) {
|
|
414
|
+
if (!existsSync(dbPath))
|
|
415
|
+
return AGENT_STORAGE_OK;
|
|
416
|
+
try {
|
|
417
|
+
const db = openStateDbReadonly(dbPath);
|
|
418
|
+
try {
|
|
419
|
+
if (!tableColumns(db, 'sessions').has('source'))
|
|
420
|
+
return { state: 'outdated', reason: `state.db has no 'source' column (older hermes-agent?). Agent sessions unavailable. Upgrade hermes-agent to fix this.` };
|
|
421
|
+
}
|
|
422
|
+
finally {
|
|
423
|
+
db.close();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
return stateDbReadFailure(error);
|
|
428
|
+
}
|
|
429
|
+
return AGENT_STORAGE_OK;
|
|
430
|
+
}
|
|
257
431
|
const warnedNoSourceColumn = new Set();
|
|
432
|
+
let readDelayMs = 0;
|
|
433
|
+
/** 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). */
|
|
434
|
+
export function setStateDbReadDelayForTests(ms) { readDelayMs = ms; }
|
|
258
435
|
/**
|
|
259
436
|
* Python `read_importable_agent_session_rows`. The recency slice is bounded to
|
|
260
437
|
* `limit * 8` candidates before the messages aggregate runs; subagent parents
|
|
@@ -269,8 +446,12 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
|
|
|
269
446
|
const limit = opts.limit === undefined ? 200 : opts.limit;
|
|
270
447
|
const exclude = opts.excludeSources === undefined ? ['cron', 'webui'] : opts.excludeSources;
|
|
271
448
|
const include = opts.includeSources ?? null;
|
|
449
|
+
if (readDelayMs > 0)
|
|
450
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, readDelayMs);
|
|
272
451
|
const db = openStateDbReadonly(dbPath);
|
|
273
452
|
try {
|
|
453
|
+
db.exec('BEGIN'); // Receipt and physical rows must belong to the same committed generation.
|
|
454
|
+
const local = localLineages(db, opts.sessionIds?.length === 1 ? opts.sessionIds[0] : undefined);
|
|
274
455
|
const sessionCols = tableColumns(db, 'sessions');
|
|
275
456
|
const messageCols = tableColumns(db, 'messages');
|
|
276
457
|
if (!sessionCols.has('source')) {
|
|
@@ -331,14 +512,26 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
|
|
|
331
512
|
let candidateOrder = 'ORDER BY s.started_at DESC';
|
|
332
513
|
const where = ['s.source IS NOT NULL'];
|
|
333
514
|
const params = [];
|
|
515
|
+
if (opts.sessionIds !== undefined) {
|
|
516
|
+
where.push('s.id IN (SELECT value FROM json_each(?))');
|
|
517
|
+
params.push(JSON.stringify(opts.sessionIds.map((id) => local.get(id)?.ids.at(-1) ?? id)));
|
|
518
|
+
}
|
|
519
|
+
const superseded = [...local].filter(([id, lineage]) => id !== lineage.ids.at(-1)).map(([id]) => id);
|
|
520
|
+
if (superseded.length) {
|
|
521
|
+
where.push('s.id NOT IN (SELECT value FROM json_each(?))');
|
|
522
|
+
params.push(JSON.stringify(superseded));
|
|
523
|
+
}
|
|
524
|
+
const hasPolicies = Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'state_meta'").get());
|
|
525
|
+
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})`;
|
|
526
|
+
const sourceSql = hasPolicies ? `COALESCE(${policySource('s.id')}, ${sessionCols.has('chat_id') ? policySource('s.chat_id') : 'NULL'}, s.source)` : 's.source';
|
|
334
527
|
const included = (include ?? []).map(String).filter(Boolean);
|
|
335
528
|
if (included.length) {
|
|
336
|
-
where.push(
|
|
529
|
+
where.push(`${sourceSql} IN (${included.map(() => '?').join(', ')})`);
|
|
337
530
|
params.push(...included);
|
|
338
531
|
}
|
|
339
532
|
const excluded = (exclude ?? []).map(String).filter(Boolean);
|
|
340
533
|
if (excluded.length) {
|
|
341
|
-
where.push(
|
|
534
|
+
where.push(`${sourceSql} NOT IN (${excluded.map(() => '?').join(', ')})`);
|
|
342
535
|
params.push(...excluded);
|
|
343
536
|
}
|
|
344
537
|
const preaggregated = messagesHasSessionId && messagesHasTimestamp && included.length === 1 && included[0] === 'cron' && !messagesIndexPresent;
|
|
@@ -351,7 +544,7 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
|
|
|
351
544
|
orderBy = messagesIndexPresent ? 'ORDER BY COALESCE(last_activity, s.started_at) DESC' : 'ORDER BY COALESCE(MAX(m.timestamp), s.started_at) DESC';
|
|
352
545
|
candidateOrder = 'ORDER BY COALESCE((SELECT MAX(mx.timestamp) FROM messages mx WHERE mx.session_id = s.id), s.started_at) DESC, s.started_at DESC';
|
|
353
546
|
}
|
|
354
|
-
const selectSql = `SELECT s.id, s.title, s.model, s.message_count, s.started_at, s.source, ${['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`;
|
|
547
|
+
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`;
|
|
355
548
|
let raw;
|
|
356
549
|
if (limit !== null) {
|
|
357
550
|
const resultLimit = Math.max(0, Math.trunc(limit));
|
|
@@ -366,8 +559,21 @@ export function readImportableAgentSessionRows(dbPath, opts = {}) {
|
|
|
366
559
|
else {
|
|
367
560
|
raw = db.prepare(`${selectSql} FROM sessions s ${joinClause} WHERE ${where.join(' AND ')} ${groupByClause} ${orderBy}`).all(...params);
|
|
368
561
|
}
|
|
369
|
-
|
|
370
|
-
|
|
562
|
+
const owners = sessionSources(db, sessionCols, raw.map((r) => str(r.id)));
|
|
563
|
+
raw = raw.map((r) => { const owner = owners.get(str(r.id)); return { ...r, source: typeof owner === 'object' ? owner.source : owner || r.source }; });
|
|
564
|
+
let projected = projectAgentSessionRows(raw.map((r) => {
|
|
565
|
+
const lineage = local.get(str(r.id));
|
|
566
|
+
if (!lineage)
|
|
567
|
+
return { ...r };
|
|
568
|
+
const root = lineage.rows[0];
|
|
569
|
+
const title = lineage.title || r.title;
|
|
570
|
+
return { ...r, title, started_at: root.started_at, last_activity: r.last_activity ?? r.started_at, parent_session_id: root.parent_session_id,
|
|
571
|
+
_lineage_root_id: lineage.owner, _lineage_tip_id: r.id, _lineage_ids: [...lineage.ids], _compression_segment_count: lineage.ids.length };
|
|
572
|
+
}));
|
|
573
|
+
projected = projected.map((r) => {
|
|
574
|
+
const parent = local.get(str(r.parent_session_id));
|
|
575
|
+
return { ...r, ...normalizeAgentSessionSource(r.source), ...(parent ? { _parent_lineage_root_id: parent.owner, parent_title: parent.title, parent_source: parent.rows.at(-1)?.source ?? null } : {}) };
|
|
576
|
+
});
|
|
371
577
|
projected = projected.filter(isCliSessionRowVisible);
|
|
372
578
|
if (limit === null)
|
|
373
579
|
return projected;
|
|
@@ -469,15 +675,138 @@ export function latestCronSessionInfo(dbPath, jobIds, completedIds) {
|
|
|
469
675
|
db.close();
|
|
470
676
|
}
|
|
471
677
|
}
|
|
472
|
-
/**
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
678
|
+
/** A source label remains display metadata; canonical receipts and ledgers independently retain write ownership. */
|
|
679
|
+
export const CANONICAL_READ_ONLY_REASON = 'This session is owned by the Agent gateway and is display-only in Talaria.';
|
|
680
|
+
export const OWNERSHIP_UNAVAILABLE_REASON = 'Agent session ownership could not be verified. The session is read-only until storage is readable.';
|
|
681
|
+
export const RETIRED_READ_ONLY_REASON = 'The Agent has retired this session identity. Open or import a new conversation to continue.';
|
|
682
|
+
/** Ownership fences outlive the fully validated localLineages history: even a pruned or broken receipt cannot release a writer. */
|
|
683
|
+
function sessionSources(db, cols, ids) {
|
|
477
684
|
const sources = new Map();
|
|
685
|
+
const canReadRows = cols.has('id') && cols.has('source');
|
|
686
|
+
const tables = new Set(db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((r) => r.name));
|
|
687
|
+
const walk = canReadRows ? resumeLineageWalker(db, cols) : (sid) => [sid];
|
|
688
|
+
const lineages = new Map(ids.map((id) => [id, walk(id)]));
|
|
689
|
+
const segments = [...new Set([...lineages.values()].flat())];
|
|
690
|
+
const rows = new Map();
|
|
691
|
+
for (let i = 0; canReadRows && i < segments.length; i += 400) {
|
|
692
|
+
const chunk = segments.slice(i, i + 400);
|
|
693
|
+
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))
|
|
694
|
+
rows.set(str(row.id), row);
|
|
695
|
+
}
|
|
696
|
+
const candidates = [...new Set([...segments, ...[...rows.values()].map((r) => str(r.chat_id)).filter(Boolean)])];
|
|
697
|
+
const candidateSet = new Set(candidates);
|
|
698
|
+
const policies = new Map();
|
|
699
|
+
const receiptOwners = new Map();
|
|
700
|
+
const retired = new Set();
|
|
701
|
+
const bindings = new Set();
|
|
702
|
+
const wanted = new Set(segments);
|
|
703
|
+
const policyValue = (value) => {
|
|
704
|
+
try {
|
|
705
|
+
const parsed = JSON.parse(str(value));
|
|
706
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
};
|
|
712
|
+
const rememberPolicy = (owner, policy) => {
|
|
713
|
+
const entry = policy?.entry;
|
|
714
|
+
const target = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry.session_id : null;
|
|
715
|
+
const lineage = Array.isArray(policy?.lineage) ? policy.lineage : [];
|
|
716
|
+
const aliases = [owner, policy?.session_id, policy?.legacy_session_id, target, ...lineage];
|
|
717
|
+
if (!candidateSet.has(owner) && !aliases.some((id) => typeof id === 'string' && wanted.has(id)))
|
|
718
|
+
return;
|
|
719
|
+
policies.set(owner, policy);
|
|
720
|
+
for (const id of aliases)
|
|
721
|
+
if (typeof id === 'string' && wanted.has(id) && !receiptOwners.has(id))
|
|
722
|
+
receiptOwners.set(id, owner);
|
|
723
|
+
};
|
|
724
|
+
if (tables.has('state_meta')) {
|
|
725
|
+
const read = db.prepare('SELECT value FROM state_meta WHERE key = ?');
|
|
726
|
+
for (const owner of candidates) {
|
|
727
|
+
const raw = read.get(`${LOCAL_POLICY_PREFIX}${owner}`);
|
|
728
|
+
if (raw)
|
|
729
|
+
rememberPolicy(owner, policyValue(raw.value));
|
|
730
|
+
if (read.get(`gateway.retired_session.v1.${owner}`))
|
|
731
|
+
retired.add(owner);
|
|
732
|
+
if (read.get(`gateway.api.binding.v1.${owner}`))
|
|
733
|
+
bindings.add(owner);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const ledgerIds = new Set();
|
|
737
|
+
for (const [table, column] of [['session_admissions', 'target_session_id'], ['worker_executions', 'session_id']]) {
|
|
738
|
+
if (!tables.has(table))
|
|
739
|
+
continue;
|
|
740
|
+
const hasLineage = tableColumns(db, table).has('lineage_json');
|
|
741
|
+
for (let i = 0; i < candidates.length; i += 400) {
|
|
742
|
+
const chunk = candidates.slice(i, i + 400);
|
|
743
|
+
const marks = chunk.map(() => '?').join(',');
|
|
744
|
+
const linked = hasLineage ? ` OR EXISTS (SELECT 1 FROM json_each(lineage_json) WHERE value IN (${marks}))` : '';
|
|
745
|
+
const found = db.prepare(`SELECT ${column} AS id, ${hasLineage ? 'lineage_json' : 'NULL AS lineage_json'} FROM ${table} WHERE ${column} IN (${marks})${linked}`).all(...chunk, ...(hasLineage ? chunk : []));
|
|
746
|
+
for (const row of found) {
|
|
747
|
+
ledgerIds.add(row.id);
|
|
748
|
+
if (row.lineage_json !== null) {
|
|
749
|
+
const lineage = JSON.parse(row.lineage_json);
|
|
750
|
+
if (!Array.isArray(lineage) || !lineage.every((v) => typeof v === 'string'))
|
|
751
|
+
throw new Error('Unreadable Agent admission lineage');
|
|
752
|
+
for (const sid of lineage)
|
|
753
|
+
ledgerIds.add(sid);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// Pruned/reset segments can survive only in a receipt, without a row or chat_id to follow.
|
|
759
|
+
if (tables.has('state_meta') && segments.some((id) => !receiptOwners.has(id) && !ledgerIds.has(id) && !retired.has(id) && !bindings.has(id))) {
|
|
760
|
+
for (const raw of db.prepare('SELECT key, value FROM state_meta WHERE key GLOB ?').iterate(`${LOCAL_POLICY_PREFIX}*`)) {
|
|
761
|
+
const policy = policyValue(raw.value);
|
|
762
|
+
const entry = policy?.entry;
|
|
763
|
+
const target = entry && typeof entry === 'object' && !Array.isArray(entry) ? entry.session_id : null;
|
|
764
|
+
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'))))
|
|
765
|
+
throw new Error('Unreadable Agent ownership receipt');
|
|
766
|
+
const owner = raw.key.slice(LOCAL_POLICY_PREFIX.length);
|
|
767
|
+
if (!owner)
|
|
768
|
+
throw new Error('Unreadable Agent ownership receipt');
|
|
769
|
+
rememberPolicy(owner, policy);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const kanban = new Set(kanbanOwnedIds(db, cols, ids.filter((id) => rows.has(id))));
|
|
773
|
+
for (const id of ids) {
|
|
774
|
+
const row = rows.get(id);
|
|
775
|
+
let source = kanban.has(id) ? 'kanban' : str(row?.source);
|
|
776
|
+
let canonical = false;
|
|
777
|
+
let retiredId = false;
|
|
778
|
+
for (const segment of lineages.get(id) ?? [id]) {
|
|
779
|
+
const chatId = str(rows.get(segment)?.chat_id);
|
|
780
|
+
for (const owner of new Set([segment, chatId, receiptOwners.get(segment)].filter((v) => Boolean(v)))) {
|
|
781
|
+
if (retired.has(owner))
|
|
782
|
+
retiredId = true;
|
|
783
|
+
if (ledgerIds.has(owner) || bindings.has(owner))
|
|
784
|
+
canonical = true;
|
|
785
|
+
if (!policies.has(owner))
|
|
786
|
+
continue;
|
|
787
|
+
const policy = policies.get(owner);
|
|
788
|
+
// A broken receipt never releases its owner or a physical row naming that owner.
|
|
789
|
+
canonical = true;
|
|
790
|
+
const origin = policy?.policy;
|
|
791
|
+
if (origin && typeof origin === 'object' && !Array.isArray(origin) && str(origin.source))
|
|
792
|
+
source = str(origin.source);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const authority = canonical;
|
|
796
|
+
// LOCAL is the canonical transport. Missing receipts cannot prove it safe to claim.
|
|
797
|
+
if (lower(source) === 'local')
|
|
798
|
+
canonical = true;
|
|
799
|
+
if (!row && !canonical && !retiredId)
|
|
800
|
+
continue;
|
|
801
|
+
sources.set(id, retiredId && !authority ? { source, canonical: true, reason: RETIRED_READ_ONLY_REASON } : canonical ? { source, canonical: true } : source);
|
|
802
|
+
}
|
|
803
|
+
return sources;
|
|
804
|
+
}
|
|
805
|
+
/** Batched source/ownership probe. An unreadable authority answers null so callers fail closed. */
|
|
806
|
+
export function stateDbSessionSources(dbPath, sessionIds) {
|
|
478
807
|
const ids = [...new Set([...sessionIds].map((s) => s.trim()).filter(Boolean))];
|
|
479
808
|
if (!ids.length || !existsSync(dbPath))
|
|
480
|
-
return
|
|
809
|
+
return new Map();
|
|
481
810
|
let db;
|
|
482
811
|
try {
|
|
483
812
|
db = openStateDbReadonly(dbPath);
|
|
@@ -486,15 +815,8 @@ export function stateDbSessionSources(dbPath, sessionIds) {
|
|
|
486
815
|
return null;
|
|
487
816
|
}
|
|
488
817
|
try {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
return sources;
|
|
492
|
-
for (let i = 0; i < ids.length; i += 500) {
|
|
493
|
-
const chunk = ids.slice(i, i + 500);
|
|
494
|
-
for (const row of db.prepare(`SELECT id, source FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')})`).all(...chunk))
|
|
495
|
-
sources.set(row.id, str(row.source));
|
|
496
|
-
}
|
|
497
|
-
return sources;
|
|
818
|
+
db.exec('BEGIN');
|
|
819
|
+
return sessionSources(db, tableColumns(db, 'sessions'), ids);
|
|
498
820
|
}
|
|
499
821
|
catch {
|
|
500
822
|
return null;
|
|
@@ -503,6 +825,76 @@ export function stateDbSessionSources(dbPath, sessionIds) {
|
|
|
503
825
|
db.close();
|
|
504
826
|
}
|
|
505
827
|
}
|
|
828
|
+
/** The same marker parser serves conservative history boundaries and exact-parent ownership walks. */
|
|
829
|
+
function explicitForkMarkers(row, includeReset) {
|
|
830
|
+
let cfg = row.model_config;
|
|
831
|
+
if (typeof cfg === 'string') {
|
|
832
|
+
try {
|
|
833
|
+
cfg = JSON.parse(cfg);
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
return [];
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg))
|
|
840
|
+
return [];
|
|
841
|
+
const c = cfg;
|
|
842
|
+
return [c._branched_from, c._delegate_from, ...(includeReset ? [c._reset_from] : [])].filter((m) => m !== undefined && m !== null);
|
|
843
|
+
}
|
|
844
|
+
/** Python `_is_explicit_fork_child_row`: a tool child, or a branch/delegate (`includeReset`: also reset) fork of its parent. */
|
|
845
|
+
function isExplicitForkChild(row, includeReset) {
|
|
846
|
+
if (str(row.source) === 'tool')
|
|
847
|
+
return true;
|
|
848
|
+
const markers = explicitForkMarkers(row, includeReset);
|
|
849
|
+
const parentId = row.parent_session_id;
|
|
850
|
+
return parentId ? markers.includes(parentId) : markers.length > 0;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Python `_resume_lineage_ids`: `sid` and the compression ancestors a resume loads, root first. Each hop's parent ended
|
|
854
|
+
* `compression` and the child is no branch, reset, delegate or tool fork. The walk stops at a cycle or the Agent's
|
|
855
|
+
* 1000-hop `_CHAIN_CAP`; without the lineage columns it is `[sid]`.
|
|
856
|
+
*/
|
|
857
|
+
function resumeLineageWalker(db, cols) {
|
|
858
|
+
if (!['parent_session_id', 'end_reason'].every((c) => cols.has(c)))
|
|
859
|
+
return (sid) => [sid];
|
|
860
|
+
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 = ?`);
|
|
861
|
+
return (sid) => {
|
|
862
|
+
const chain = [sid];
|
|
863
|
+
let current = select.get(sid);
|
|
864
|
+
if (!current || isExplicitForkChild(current, false))
|
|
865
|
+
return chain;
|
|
866
|
+
while (chain.length < 1000) {
|
|
867
|
+
const parentId = str(current.parent_session_id);
|
|
868
|
+
if (!parentId || chain.includes(parentId) || isExplicitForkChild(current, true))
|
|
869
|
+
break;
|
|
870
|
+
const parent = select.get(parentId);
|
|
871
|
+
if (str(parent?.end_reason) !== 'compression')
|
|
872
|
+
break;
|
|
873
|
+
chain.push(parentId);
|
|
874
|
+
current = parent;
|
|
875
|
+
}
|
|
876
|
+
return chain.reverse();
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* TAL-793 (Agent `is_kanban_owned_session`): the `ids` whose compression lineage has a `kanban` `source` or
|
|
881
|
+
* `created_source` (schema v31; `source` alone decides before it), a Kanban worker's transcript only its dispatcher continues.
|
|
882
|
+
*/
|
|
883
|
+
function kanbanOwnedIds(db, cols, ids) {
|
|
884
|
+
if (!ids.length)
|
|
885
|
+
return [];
|
|
886
|
+
const walk = resumeLineageWalker(db, cols);
|
|
887
|
+
const lineages = ids.map(walk);
|
|
888
|
+
const segments = [...new Set(lineages.flat())];
|
|
889
|
+
const match = cols.has('created_source') ? "(source = 'kanban' OR created_source = 'kanban')" : "source = 'kanban'";
|
|
890
|
+
const kanban = new Set();
|
|
891
|
+
for (let i = 0; i < segments.length; i += 500) {
|
|
892
|
+
const chunk = segments.slice(i, i + 500);
|
|
893
|
+
for (const row of db.prepare(`SELECT id FROM sessions WHERE id IN (${chunk.map(() => '?').join(',')}) AND ${match}`).all(...chunk))
|
|
894
|
+
kanban.add(row.id);
|
|
895
|
+
}
|
|
896
|
+
return ids.filter((_, i) => lineages[i].some((sid) => kanban.has(sid)));
|
|
897
|
+
}
|
|
506
898
|
/** Python `state_db_has_session`: true only when `sid` is a row of the sessions table; a missing or unreadable db is false. */
|
|
507
899
|
export function stateDbHasSession(dbPath, sid) {
|
|
508
900
|
const id = sid.trim();
|
|
@@ -535,7 +927,7 @@ export function stateDbHasSession(dbPath, sid) {
|
|
|
535
927
|
export function cheapChangeFingerprint(dbPath, onError) {
|
|
536
928
|
try {
|
|
537
929
|
const parts = [];
|
|
538
|
-
const db = openStateDbReadonly(dbPath);
|
|
930
|
+
const db = openStateDbReadonly(dbPath, STATE_DB_FINGERPRINT_BUSY_TIMEOUT_MS);
|
|
539
931
|
try {
|
|
540
932
|
for (const table of ['sessions', 'messages']) {
|
|
541
933
|
try {
|
|
@@ -579,19 +971,143 @@ export function cheapChangeFingerprint(dbPath, onError) {
|
|
|
579
971
|
return null;
|
|
580
972
|
}
|
|
581
973
|
}
|
|
974
|
+
let listingWorker = null;
|
|
975
|
+
let nextCallId = 0;
|
|
976
|
+
/** Set while `stopStateDbWorker` waits for the worker to exit; calls then reject instead of starting another worker. */
|
|
977
|
+
let stoppingWorker = false;
|
|
978
|
+
/** Reject every call still waiting on `current` and forget it, so the next call starts a fresh worker. */
|
|
979
|
+
function retireListingWorker(current, error) {
|
|
980
|
+
if (listingWorker === current)
|
|
981
|
+
listingWorker = null;
|
|
982
|
+
for (const call of current.pending.values())
|
|
983
|
+
call.reject(error);
|
|
984
|
+
current.pending.clear();
|
|
985
|
+
}
|
|
986
|
+
/** The lazily started, long-lived listing worker; requests run one at a time in arrival order. */
|
|
987
|
+
function startedListingWorker() {
|
|
988
|
+
if (listingWorker)
|
|
989
|
+
return listingWorker;
|
|
990
|
+
// Source runs (vitest) load the TypeScript worker; the build runs the compiled one beside this module.
|
|
991
|
+
const ext = import.meta.url.endsWith('.ts') ? 'ts' : 'js';
|
|
992
|
+
const current = { worker: new Worker(new URL(`./state-db-worker.${ext}`, import.meta.url)), pending: new Map() };
|
|
993
|
+
current.worker.unref();
|
|
994
|
+
current.worker.on('message', (reply) => {
|
|
995
|
+
const call = current.pending.get(reply.id);
|
|
996
|
+
if (!call)
|
|
997
|
+
return;
|
|
998
|
+
current.pending.delete(reply.id);
|
|
999
|
+
if (!current.pending.size)
|
|
1000
|
+
current.worker.unref();
|
|
1001
|
+
call.resolve(reply);
|
|
1002
|
+
});
|
|
1003
|
+
current.worker.on('error', (error) => { retireListingWorker(current, error); });
|
|
1004
|
+
current.worker.on('exit', (code) => { retireListingWorker(current, new Error(`state.db worker exited with code ${String(code)}`)); });
|
|
1005
|
+
listingWorker = current;
|
|
1006
|
+
return current;
|
|
1007
|
+
}
|
|
1008
|
+
function callListingWorker(call) {
|
|
1009
|
+
// A multi-read caller the stop interrupted goes on to its next read; that read must not restart the worker.
|
|
1010
|
+
if (stoppingWorker)
|
|
1011
|
+
return Promise.reject(new Error('state.db worker stopped'));
|
|
1012
|
+
const current = startedListingWorker();
|
|
1013
|
+
const id = ++nextCallId;
|
|
1014
|
+
return new Promise((resolve, reject) => {
|
|
1015
|
+
current.pending.set(id, { resolve, reject });
|
|
1016
|
+
// A call in flight keeps the process alive until it settles; an idle worker never does.
|
|
1017
|
+
current.worker.ref();
|
|
1018
|
+
current.worker.postMessage({ ...call, id, delayMs: readDelayMs });
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
/** `readImportableAgentSessionRows` on the listing worker, so a slow `state.db` never blocks the event loop. Rejects when the worker fails. */
|
|
1022
|
+
export async function readImportableAgentSessionRowsAsync(dbPath, opts = {}) {
|
|
1023
|
+
const { log, ...query } = opts;
|
|
1024
|
+
const reply = await callListingWorker({ op: 'rows', dbPath, opts: query });
|
|
1025
|
+
for (const line of reply.logs)
|
|
1026
|
+
log?.(line);
|
|
1027
|
+
if (reply.error !== undefined)
|
|
1028
|
+
throw new Error(reply.error);
|
|
1029
|
+
return reply.value;
|
|
1030
|
+
}
|
|
1031
|
+
/** `cheapChangeFingerprint` on the listing worker; a worker failure reports as a failed change check. */
|
|
1032
|
+
export async function cheapChangeFingerprintAsync(dbPath, onError) {
|
|
1033
|
+
let reply;
|
|
1034
|
+
try {
|
|
1035
|
+
reply = await callListingWorker({ op: 'fingerprint', dbPath });
|
|
1036
|
+
}
|
|
1037
|
+
catch (error) {
|
|
1038
|
+
onError?.('change check failed', error);
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
for (const [reason, message] of reply.failures)
|
|
1042
|
+
onError?.(reason, new Error(message));
|
|
1043
|
+
return reply.value;
|
|
1044
|
+
}
|
|
1045
|
+
/** `stateDbStorage` on the listing worker; a worker failure is `unavailable`. */
|
|
1046
|
+
export async function stateDbStorageAsync(dbPath) {
|
|
1047
|
+
try {
|
|
1048
|
+
return (await callListingWorker({ op: 'storage', dbPath })).value;
|
|
1049
|
+
}
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
return stateDbReadFailure(error);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
/** `stateDbSessionSources` on the listing worker; a worker failure is an unreadable state.db (null), so owners stay locked. */
|
|
1055
|
+
export async function stateDbSessionSourcesAsync(dbPath, sessionIds) {
|
|
1056
|
+
try {
|
|
1057
|
+
return (await callListingWorker({ op: 'sources', dbPath, ids: [...sessionIds] })).value;
|
|
1058
|
+
}
|
|
1059
|
+
catch {
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/** `agentSessionRowsExisting` on the listing worker; a worker failure assumes every row present, so nothing is pruned. */
|
|
1064
|
+
export async function agentSessionRowsExistingAsync(dbPath, sessionIds) {
|
|
1065
|
+
const ids = [...sessionIds];
|
|
1066
|
+
try {
|
|
1067
|
+
return (await callListingWorker({ op: 'existing', dbPath, ids })).value;
|
|
1068
|
+
}
|
|
1069
|
+
catch {
|
|
1070
|
+
return new Set(ids.map((s) => s.trim()).filter(Boolean));
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/** Test hook: the listing worker exits abruptly while this call is in flight. */
|
|
1074
|
+
export function crashStateDbWorkerForTests() {
|
|
1075
|
+
return callListingWorker({ op: 'crash' });
|
|
1076
|
+
}
|
|
1077
|
+
/** Stop the listing worker (server shutdown). Calls in flight reject; a later call starts a new worker. */
|
|
1078
|
+
export async function stopStateDbWorker() {
|
|
1079
|
+
const current = listingWorker;
|
|
1080
|
+
if (!current)
|
|
1081
|
+
return;
|
|
1082
|
+
stoppingWorker = true;
|
|
1083
|
+
try {
|
|
1084
|
+
retireListingWorker(current, new Error('state.db worker stopped'));
|
|
1085
|
+
await current.worker.terminate();
|
|
1086
|
+
}
|
|
1087
|
+
finally {
|
|
1088
|
+
stoppingWorker = false;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
582
1091
|
// ── message reader ────────────────────────────────────────────────────────
|
|
583
1092
|
const STATE_DB_CONTENT_JSON_PREFIX = '\0json:';
|
|
584
1093
|
// `display_kind` / `display_metadata`: the Agent's own tag for rows it writes itself (steers, delivered notifications; TAL-371).
|
|
585
|
-
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'];
|
|
586
|
-
const JSON_MESSAGE_COLUMNS = new Set(['tool_calls', 'reasoning_details', 'codex_reasoning_items', 'codex_message_items', 'display_metadata']);
|
|
1094
|
+
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'];
|
|
1095
|
+
const JSON_MESSAGE_COLUMNS = new Set(['tool_calls', 'reasoning_details', 'codex_reasoning_items', 'codex_message_items', 'display_metadata', 'absorbed_message_uids']);
|
|
587
1096
|
/** Python `_decode_state_db_content`: the Agent's sentinel-prefixed structured content, left untouched otherwise. */
|
|
588
1097
|
function decodeStateDbContent(value) {
|
|
589
1098
|
const text = Buffer.isBuffer(value) ? value.toString('utf8') : value;
|
|
590
1099
|
if (typeof text !== 'string' || !text.startsWith(STATE_DB_CONTENT_JSON_PREFIX))
|
|
591
1100
|
return text;
|
|
592
1101
|
try {
|
|
593
|
-
const
|
|
594
|
-
|
|
1102
|
+
const payload = text.slice(STATE_DB_CONTENT_JSON_PREFIX.length);
|
|
1103
|
+
let unsafeInteger = false;
|
|
1104
|
+
const decoded = JSON.parse(payload, (_key, value, context) => {
|
|
1105
|
+
if (typeof value === 'number' && !Number.isSafeInteger(value) && /^-?\d+$/.test(context?.source ?? ''))
|
|
1106
|
+
unsafeInteger = true;
|
|
1107
|
+
return value;
|
|
1108
|
+
});
|
|
1109
|
+
// Python keeps arbitrary-precision integers. Retain the JSON text rather than silently rounding any of them.
|
|
1110
|
+
return unsafeInteger ? payload : decoded;
|
|
595
1111
|
}
|
|
596
1112
|
catch {
|
|
597
1113
|
return text;
|
|
@@ -616,11 +1132,13 @@ function projectStateDbMessage(row, hasId) {
|
|
|
616
1132
|
const value = row[col];
|
|
617
1133
|
if (value === null || value === undefined || value === '')
|
|
618
1134
|
continue;
|
|
619
|
-
msg[col] = JSON_MESSAGE_COLUMNS.has(col) ? jsonLoadsIfString(value) : value;
|
|
1135
|
+
msg[col === 'absorbed_message_uids' ? '_absorbed_message_uids' : col] = JSON_MESSAGE_COLUMNS.has(col) ? jsonLoadsIfString(value) : value;
|
|
620
1136
|
}
|
|
621
1137
|
// TAL-493: the merge tells rows committed after the session's last read by this id.
|
|
622
1138
|
if (hasId && row.id !== null && row.id !== undefined)
|
|
623
1139
|
msg._state_db_row_id = row.id;
|
|
1140
|
+
if (typeof row.message_uid === 'string' && row.message_uid.length)
|
|
1141
|
+
msg._state_db_message_uid = row.message_uid;
|
|
624
1142
|
if (msg.role === 'tool' && msg.tool_name && !msg.name)
|
|
625
1143
|
msg.name = msg.tool_name;
|
|
626
1144
|
return msg;
|
|
@@ -639,6 +1157,74 @@ function stateDbGeneration(dbPath) {
|
|
|
639
1157
|
return null;
|
|
640
1158
|
}
|
|
641
1159
|
}
|
|
1160
|
+
// Agent v0.21.6's exact in-flight request wrapper. Unknown rewrites are not copy evidence.
|
|
1161
|
+
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.]';
|
|
1162
|
+
/** The payload comparison ignores only the Agent's documented replay wrapper and dropped api_content. */
|
|
1163
|
+
function replayPayload(row) {
|
|
1164
|
+
const message = projectStateDbMessage(row, false);
|
|
1165
|
+
delete message.timestamp;
|
|
1166
|
+
const copyKey = JSON.stringify(message);
|
|
1167
|
+
let content = message.content;
|
|
1168
|
+
let replay = false;
|
|
1169
|
+
if (row.role === 'user') {
|
|
1170
|
+
if (typeof content === 'string') {
|
|
1171
|
+
replay = content.startsWith(`${INFLIGHT_REPLAY_HEADER}\n`);
|
|
1172
|
+
content = (replay ? content.slice(INFLIGHT_REPLAY_HEADER.length + 1) : content).trim();
|
|
1173
|
+
}
|
|
1174
|
+
else if (Array.isArray(content)) {
|
|
1175
|
+
// Multimodal replays prepend a text block and retain all original parts, including media.
|
|
1176
|
+
const parts = content;
|
|
1177
|
+
let start = 0;
|
|
1178
|
+
while (start < parts.length) {
|
|
1179
|
+
const first = parts[start];
|
|
1180
|
+
if (!first || typeof first !== 'object' || Array.isArray(first))
|
|
1181
|
+
break;
|
|
1182
|
+
const part = first;
|
|
1183
|
+
if (part.type !== 'text' || part.text !== `${INFLIGHT_REPLAY_HEADER}\n` || Object.keys(part).some((k) => k !== 'type' && k !== 'text'))
|
|
1184
|
+
break;
|
|
1185
|
+
start += 1;
|
|
1186
|
+
}
|
|
1187
|
+
replay = start > 0;
|
|
1188
|
+
content = parts.slice(start);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
message.content = content;
|
|
1192
|
+
delete message.api_content; // _reappend_inflight_user_task drops this stale model-facing sidecar.
|
|
1193
|
+
return { copyKey, replayKey: JSON.stringify(message), replay };
|
|
1194
|
+
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Compare adjacent generations, never all rows of one UID with its oldest archive. The archive retains
|
|
1197
|
+
* authored payload changes even after active/compacted flags change. Only unchanged timestamp copies or
|
|
1198
|
+
* exact request restatements inherit identity; edits (including A -> B -> A) start at their own row ID.
|
|
1199
|
+
*/
|
|
1200
|
+
function replayIdentities(db, chain, rows, available) {
|
|
1201
|
+
const identities = new Map();
|
|
1202
|
+
// Tool-result text can be pruned without changing its tool-call identity; retain the existing rule.
|
|
1203
|
+
const uids = [...new Set(rows.filter((row) => row.role !== 'tool').map((row) => row.message_uid).filter((uid) => typeof uid === 'string' && uid.length > 0))];
|
|
1204
|
+
if (!uids.length)
|
|
1205
|
+
return identities;
|
|
1206
|
+
const liveIds = new Set(rows.map((row) => row.id));
|
|
1207
|
+
const previous = new Map();
|
|
1208
|
+
const columns = ['id', 'message_uid', 'active', 'compacted', 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
|
|
1209
|
+
// One ordered scan of the selected sessions, filtering against a materialized UID set. Stream the rows;
|
|
1210
|
+
// memory holds only the preceding payload per live UID, not the accumulated archive. NULL/nonstandard
|
|
1211
|
+
// active flags take exactly the same path and do not depend on the Agent's partial active=1 UID index.
|
|
1212
|
+
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`);
|
|
1213
|
+
for (const row of history.iterate(...chain, JSON.stringify(uids))) {
|
|
1214
|
+
const uid = row.message_uid;
|
|
1215
|
+
const payload = replayPayload(row);
|
|
1216
|
+
const prior = previous.get(uid);
|
|
1217
|
+
const copy = payload.copyKey === prior?.copyKey && row.timestamp === prior.timestamp;
|
|
1218
|
+
const restatement = prior && payload.replay && payload.replayKey === prior.replayKey && (prior.active !== 0 || prior.compacted === 1);
|
|
1219
|
+
const identity = prior && (copy || restatement) ? prior.identity : Number(row.id);
|
|
1220
|
+
const changed = prior ? (copy || restatement ? prior.changed : true) : false;
|
|
1221
|
+
const sourceKey = prior && (copy || restatement) ? prior.sourceKey : stateDbMessageFingerprint(projectStateDbMessage(row, false));
|
|
1222
|
+
if (prior && row.role !== 'tool' && liveIds.has(row.id))
|
|
1223
|
+
identities.set(Number(row.id), { id: identity, changed, sourceKey });
|
|
1224
|
+
previous.set(uid, { ...payload, identity, changed, sourceKey, timestamp: row.timestamp, active: row.active, compacted: row.compacted });
|
|
1225
|
+
}
|
|
1226
|
+
return identities;
|
|
1227
|
+
}
|
|
642
1228
|
export function stateDbSessionMessages(dbPath, sid, opts = {}) {
|
|
643
1229
|
return stateDbSessionRead(dbPath, sid, opts).rows;
|
|
644
1230
|
}
|
|
@@ -647,7 +1233,14 @@ export function stateDbSessionMessages(dbPath, sid, opts = {}) {
|
|
|
647
1233
|
* (TAL-493: an empty id-capable read is a baseline of 0; a missing database or a failed read says nothing).
|
|
648
1234
|
*/
|
|
649
1235
|
export function stateDbSessionRead(dbPath, sid, opts = {}) {
|
|
650
|
-
|
|
1236
|
+
return readStateDbSession(dbPath, sid, opts);
|
|
1237
|
+
}
|
|
1238
|
+
/** The same snapshot and provenance projection, restricted to physical rows past the seen marker. */
|
|
1239
|
+
export function stateDbSessionTailRead(dbPath, sid, afterId, opts = {}) {
|
|
1240
|
+
return readStateDbSession(dbPath, sid, { ...opts, stitch: false, afterId });
|
|
1241
|
+
}
|
|
1242
|
+
function readStateDbSession(dbPath, sid, opts) {
|
|
1243
|
+
const none = { rows: [], idCapable: false, ok: false, reason: null, generation: null };
|
|
651
1244
|
const id = sid.trim();
|
|
652
1245
|
if (!id || !existsSync(dbPath))
|
|
653
1246
|
return none;
|
|
@@ -659,20 +1252,23 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
|
|
|
659
1252
|
return none;
|
|
660
1253
|
}
|
|
661
1254
|
try {
|
|
1255
|
+
db.exec('BEGIN'); // The live rows and their archived predecessors must describe one snapshot.
|
|
662
1256
|
const available = tableColumns(db, 'messages');
|
|
663
1257
|
if (!['role', 'content', 'timestamp'].every((c) => available.has(c)))
|
|
664
1258
|
return { ...none, ok: available.size > 0 };
|
|
665
1259
|
const hasId = available.has('id');
|
|
666
|
-
const selected = [...(hasId ? ['id'] : []), 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
|
|
1260
|
+
const selected = [...(hasId ? ['id'] : []), ...(available.has('message_uid') ? ['message_uid'] : []), 'role', 'content', 'timestamp', ...OPTIONAL_MESSAGE_COLUMNS.filter((c) => available.has(c))];
|
|
667
1261
|
// TAL-529: an explicit lineage (the session and its compression continuations) replaces the parent walk.
|
|
668
|
-
const
|
|
669
|
-
|
|
1262
|
+
const local = localLineages(db, id).get(id);
|
|
1263
|
+
const chain = local ? (opts.stitch === false ? [local.ids.at(-1) ?? id] : [...local.context]) : opts.lineage?.length ? [...opts.lineage] : [id];
|
|
1264
|
+
if (!local && !opts.lineage?.length && (opts.stitch ?? true)) {
|
|
670
1265
|
const sessionCols = tableColumns(db, 'sessions');
|
|
671
1266
|
if (['parent_session_id', 'end_reason', 'started_at', 'source'].every((c) => sessionCols.has(c))) {
|
|
672
|
-
const
|
|
1267
|
+
const extra = ['model_config', 'session_source'].map((c) => sessionCols.has(c) ? c : `NULL AS ${c}`).join(', ');
|
|
1268
|
+
const select = db.prepare(`SELECT id, source, started_at, parent_session_id, ended_at, end_reason, ${extra} FROM sessions WHERE id = ?`);
|
|
673
1269
|
let current = select.get(id);
|
|
674
1270
|
const seen = new Set([id]);
|
|
675
|
-
for (let hop = 0; hop <
|
|
1271
|
+
for (let hop = 0; hop < COMPRESSION_CHAIN_CAP && current; hop += 1) {
|
|
676
1272
|
const parentId = str(current.parent_session_id);
|
|
677
1273
|
if (!parentId || seen.has(parentId))
|
|
678
1274
|
break;
|
|
@@ -686,19 +1282,74 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
|
|
|
686
1282
|
}
|
|
687
1283
|
}
|
|
688
1284
|
const activeClause = available.has('active') ? ' AND (active IS NULL OR active != 0)' : '';
|
|
1285
|
+
if (opts.afterId !== undefined && !hasId)
|
|
1286
|
+
return { ...none, ok: true };
|
|
1287
|
+
const range = opts.afterId === undefined ? '' : ' AND id > ?';
|
|
1288
|
+
const rangeArgs = opts.afterId === undefined ? [] : [opts.afterId];
|
|
689
1289
|
const order = hasId ? 'id' : 'timestamp';
|
|
690
|
-
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);
|
|
1290
|
+
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);
|
|
691
1291
|
const projected = rows.map((row) => projectStateDbMessage(row, hasId));
|
|
1292
|
+
// TAL-709: an in-place compaction archives the live rows and re-inserts each as a copy that keeps its role and timestamp
|
|
1293
|
+
// (content may be pruned). A copy takes the id of the oldest archived row it copies, so the merge sees a row it has
|
|
1294
|
+
// already read instead of a new one. Only a compaction archive (`compacted = 1`) vouches for a changed copy: a rewound
|
|
1295
|
+
// row (`compacted = 0`, also the compaction's carried tail) counts only for the same tool call or the same content, so
|
|
1296
|
+
// an edit that kept its timestamp stays new.
|
|
1297
|
+
if (hasId && available.has('active')) {
|
|
1298
|
+
// One indexed lookup per live row ((session_id, active, timestamp)): the archive is never loaded, however many
|
|
1299
|
+
// compactions it holds.
|
|
1300
|
+
const sameCall = available.has('tool_call_id') ? " AND COALESCE(a.tool_call_id, '') = COALESCE(m.tool_call_id, '')" : '';
|
|
1301
|
+
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 ');
|
|
1302
|
+
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);
|
|
1303
|
+
const twinOf = new Map(twins.map((row) => [row.id, row.twin]));
|
|
1304
|
+
for (const [i, row] of rows.entries()) {
|
|
1305
|
+
const twin = twinOf.get(row.id);
|
|
1306
|
+
if (typeof twin === 'number')
|
|
1307
|
+
projected[i]._state_db_row_id = twin;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (hasId && ['message_uid', 'active', 'compacted'].every((c) => available.has(c))) {
|
|
1311
|
+
const identities = replayIdentities(db, local?.context ?? chain, rows, available);
|
|
1312
|
+
for (const [i, row] of rows.entries()) {
|
|
1313
|
+
const identity = identities.get(Number(row.id));
|
|
1314
|
+
// A known changed generation overrides even a coincidental legacy timestamp twin.
|
|
1315
|
+
if (identity !== undefined) {
|
|
1316
|
+
projected[i]._state_db_row_id = identity.id;
|
|
1317
|
+
if (identity.id !== row.id)
|
|
1318
|
+
projected[i]._state_db_source_key = identity.sourceKey;
|
|
1319
|
+
if (identity.changed)
|
|
1320
|
+
projected[i]._state_db_authored_change = true;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
692
1324
|
// TAL-493: the database file's identity, so a marker taken in a replaced state.db is never applied to its successor.
|
|
1325
|
+
const generation = stateDbGeneration(dbPath);
|
|
693
1326
|
if (hasId) {
|
|
694
|
-
const generation = stateDbGeneration(dbPath);
|
|
695
1327
|
for (const m of projected)
|
|
696
1328
|
m._state_db_generation = generation;
|
|
697
1329
|
}
|
|
698
|
-
|
|
1330
|
+
let anchor;
|
|
1331
|
+
if (opts.afterId !== undefined) {
|
|
1332
|
+
anchor = projected.find((m) => m._state_db_row_id === opts.afterId);
|
|
1333
|
+
if (!anchor) {
|
|
1334
|
+
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);
|
|
1335
|
+
if (row)
|
|
1336
|
+
anchor = { ...projectStateDbMessage(row, true), _state_db_generation: generation };
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
let requiresFullRead = false;
|
|
1340
|
+
if (opts.afterId !== undefined && opts.unidentified) {
|
|
1341
|
+
const matches = projected.filter((m) => opts.unidentified.some((keys) => keys.has(stateDbMessageKey(m))));
|
|
1342
|
+
if (matches.length) {
|
|
1343
|
+
// Projection can normalize content/timestamps. A bounded role-key superset is conservative copy detection.
|
|
1344
|
+
const roles = [...new Set(matches.map((m) => m.role))];
|
|
1345
|
+
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));
|
|
1346
|
+
requiresFullRead = Boolean(covered) || available.has('message_uid');
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return { rows: projected, idCapable: hasId, ok: true, reason: null, generation, requiresFullRead, ...(anchor ? { anchor } : {}) };
|
|
699
1350
|
}
|
|
700
|
-
catch {
|
|
701
|
-
return none;
|
|
1351
|
+
catch (error) {
|
|
1352
|
+
return { ...none, reason: stateDbReadFailure(error).reason };
|
|
702
1353
|
}
|
|
703
1354
|
finally {
|
|
704
1355
|
db.close();
|
|
@@ -706,8 +1357,8 @@ export function stateDbSessionRead(dbPath, sid, opts = {}) {
|
|
|
706
1357
|
}
|
|
707
1358
|
/**
|
|
708
1359
|
* TAL-529: `sid` and each compression continuation the Agent rotated it to, down to the live tip (Python
|
|
709
|
-
* `resolve_live_compression_tip`). The walk stops at the last unambiguous row: no continuation, several, a cycle, or
|
|
710
|
-
* hops. A missing or unreadable database is `[sid]`.
|
|
1360
|
+
* `resolve_live_compression_tip`). The walk stops at the last unambiguous row: no continuation, several, a cycle, or
|
|
1361
|
+
* `COMPRESSION_CHAIN_CAP` hops. A missing or unreadable database is `[sid]`.
|
|
711
1362
|
*/
|
|
712
1363
|
export function stateDbCompressionLineage(dbPath, sid) {
|
|
713
1364
|
const lineage = [sid.trim()];
|
|
@@ -721,14 +1372,19 @@ export function stateDbCompressionLineage(dbPath, sid) {
|
|
|
721
1372
|
return lineage;
|
|
722
1373
|
}
|
|
723
1374
|
try {
|
|
1375
|
+
db.exec('BEGIN');
|
|
1376
|
+
const local = localLineages(db, lineage[0]).get(lineage[0]);
|
|
1377
|
+
if (local)
|
|
1378
|
+
return [...local.ids];
|
|
724
1379
|
const sessionCols = tableColumns(db, 'sessions');
|
|
725
1380
|
if (!['id', 'parent_session_id', 'end_reason', 'ended_at', 'started_at', 'source'].every((c) => sessionCols.has(c)))
|
|
726
1381
|
return lineage;
|
|
727
1382
|
const sessionSource = sessionCols.has('session_source') ? 'session_source' : 'NULL AS session_source';
|
|
728
|
-
const
|
|
1383
|
+
const modelConfig = sessionCols.has('model_config') ? 'model_config' : 'NULL AS model_config';
|
|
1384
|
+
const cols = `id, source, ${sessionSource}, started_at, parent_session_id, ended_at, end_reason, ${modelConfig}`;
|
|
729
1385
|
let current = db.prepare(`SELECT ${cols} FROM sessions WHERE id = ?`).get(lineage[0]);
|
|
730
1386
|
const children = db.prepare(`SELECT ${cols} FROM sessions WHERE parent_session_id = ?`);
|
|
731
|
-
for (let hop = 0; hop <
|
|
1387
|
+
for (let hop = 0; hop < COMPRESSION_CHAIN_CAP && current; hop += 1) {
|
|
732
1388
|
const parent = current;
|
|
733
1389
|
const next = children.all(str(parent.id)).filter((child) => isContinuationSession(parent, child));
|
|
734
1390
|
if (next.length !== 1 || lineage.includes(str(next[0].id)))
|
|
@@ -745,6 +1401,93 @@ export function stateDbCompressionLineage(dbPath, sid) {
|
|
|
745
1401
|
db.close();
|
|
746
1402
|
}
|
|
747
1403
|
}
|
|
1404
|
+
function lineageReportRow(row, role) {
|
|
1405
|
+
const ended = row.ended_at ?? null;
|
|
1406
|
+
return {
|
|
1407
|
+
session_id: row.id, role, title: row.title ?? null, source: row.source ?? null, started_at: row.started_at ?? null, updated_at: ended ?? row.started_at ?? null,
|
|
1408
|
+
end_reason: row.end_reason ?? null, active: ended === null, archived: false,
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
const emptyLineageReport = (sid) => ({
|
|
1412
|
+
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,
|
|
1413
|
+
});
|
|
1414
|
+
/**
|
|
1415
|
+
* Python `read_session_lineage_report`: a read-only report of `sid`'s compression lineage. The walk follows up to
|
|
1416
|
+
* `maxHops` continuation parents (the tip first, the root last), then lists each segment's non-continuation children,
|
|
1417
|
+
* newest first. A cycle, a hop limit hit, or a continuation off the walked path marks `manual_review`. A missing,
|
|
1418
|
+
* unreadable, or too-old database, or an unknown id, answers `found: false`.
|
|
1419
|
+
*/
|
|
1420
|
+
export function stateDbLineageReport(dbPath, rawSid, maxHops = COMPRESSION_CHAIN_CAP) {
|
|
1421
|
+
const sid = rawSid.trim();
|
|
1422
|
+
if (!sid || !existsSync(dbPath))
|
|
1423
|
+
return emptyLineageReport(sid);
|
|
1424
|
+
let db;
|
|
1425
|
+
try {
|
|
1426
|
+
db = openStateDbReadonly(dbPath);
|
|
1427
|
+
}
|
|
1428
|
+
catch {
|
|
1429
|
+
return emptyLineageReport(sid);
|
|
1430
|
+
}
|
|
1431
|
+
try {
|
|
1432
|
+
db.exec('BEGIN');
|
|
1433
|
+
const local = localLineages(db, sid).get(sid);
|
|
1434
|
+
const cols = tableColumns(db, 'sessions');
|
|
1435
|
+
if (!['id', 'parent_session_id', 'end_reason'].every((c) => cols.has(c)))
|
|
1436
|
+
return emptyLineageReport(sid);
|
|
1437
|
+
const col = (name, fallback = 'NULL') => (cols.has(name) ? `s.${name}` : `${fallback} AS ${name}`);
|
|
1438
|
+
const select = `SELECT s.id, ${col('source')}, ${col('session_source')}, ${col('title')}, ${col('started_at', '0')}, s.parent_session_id, ${col('ended_at')}, s.end_reason FROM sessions s`;
|
|
1439
|
+
const byId = db.prepare(`${select} WHERE s.id = ?`);
|
|
1440
|
+
const fetch = (id) => (id ? byId.get(str(id)) ?? null : null);
|
|
1441
|
+
const target = fetch(sid);
|
|
1442
|
+
if (!target)
|
|
1443
|
+
return emptyLineageReport(sid);
|
|
1444
|
+
const segments = local ? [...local.rows].reverse().slice(0, Math.max(1, maxHops)) : [target];
|
|
1445
|
+
const seen = new Set(segments.map((row) => str(row.id)));
|
|
1446
|
+
let manualReview = local ? local.ids.length > maxHops : true;
|
|
1447
|
+
for (let hop = 0; !local && hop < maxHops; hop += 1) {
|
|
1448
|
+
const current = segments[segments.length - 1];
|
|
1449
|
+
const parentId = str(current.parent_session_id);
|
|
1450
|
+
const parent = fetch(parentId);
|
|
1451
|
+
if (!parent || seen.has(parentId)) {
|
|
1452
|
+
manualReview = Boolean(parentId && seen.has(parentId));
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
if (!isContinuationSession(parent, current)) {
|
|
1456
|
+
manualReview = false;
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
segments.push(parent);
|
|
1460
|
+
seen.add(parentId);
|
|
1461
|
+
}
|
|
1462
|
+
const segmentIds = segments.map((row) => str(row.id));
|
|
1463
|
+
const children = db.prepare(`${select} WHERE s.parent_session_id IN (${segmentIds.map(() => '?').join(', ')})`).all(...segmentIds);
|
|
1464
|
+
const childRows = [];
|
|
1465
|
+
for (const parent of segments) {
|
|
1466
|
+
const own = children.filter((child) => str(child.parent_session_id) === str(parent.id) && !seen.has(str(child.id)));
|
|
1467
|
+
own.sort((a, b) => (Number(b.started_at) || 0) - (Number(a.started_at) || 0));
|
|
1468
|
+
for (const child of own) {
|
|
1469
|
+
// A continuation off the walked path means a branched lineage or an older selected segment: flag it, list nothing.
|
|
1470
|
+
if (isContinuationSession(parent, child))
|
|
1471
|
+
manualReview = true;
|
|
1472
|
+
else
|
|
1473
|
+
childRows.push(child);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return {
|
|
1477
|
+
mutation: false, found: true, session_id: sid, lineage_key: segments[segments.length - 1].id, tip_session_id: segments[0].id,
|
|
1478
|
+
total_segments: segments.length, materialized_segments: segments.length,
|
|
1479
|
+
segments: segments.map((row, idx) => lineageReportRow(row, idx === 0 ? 'tip' : 'hidden_segment')),
|
|
1480
|
+
children: childRows.map((row) => lineageReportRow(row, 'child_session')),
|
|
1481
|
+
manual_review: manualReview,
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
catch {
|
|
1485
|
+
return emptyLineageReport(sid);
|
|
1486
|
+
}
|
|
1487
|
+
finally {
|
|
1488
|
+
db.close();
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
748
1491
|
/** The `sessions` row for `sid` (source and lifecycle columns), or null. */
|
|
749
1492
|
export function stateDbSessionRow(dbPath, sid) {
|
|
750
1493
|
const id = sid.trim();
|
|
@@ -821,4 +1564,78 @@ export function insightsSessionRows(dbPath, cutoff) {
|
|
|
821
1564
|
db.close();
|
|
822
1565
|
}
|
|
823
1566
|
}
|
|
1567
|
+
/** A state.db `timestamp` in unix seconds: a number, or an ISO-8601 string (Python `fromisoformat`); null when unreadable. */
|
|
1568
|
+
export function stateDbTimestampSeconds(value) {
|
|
1569
|
+
if (typeof value === 'number')
|
|
1570
|
+
return Number.isFinite(value) ? value : null;
|
|
1571
|
+
if (typeof value !== 'string' || !value.trim())
|
|
1572
|
+
return null;
|
|
1573
|
+
const ms = Date.parse(value.trim());
|
|
1574
|
+
return Number.isFinite(ms) ? ms / 1000 : null;
|
|
1575
|
+
}
|
|
1576
|
+
/** Python `CONVERSATION_ROUND_THRESHOLD`: the handoff dock is offered from this many rounds. */
|
|
1577
|
+
export const CONVERSATION_ROUND_THRESHOLD = 10;
|
|
1578
|
+
/**
|
|
1579
|
+
* Python `count_conversation_rounds` (TAL-258): a round is a user message answered by the assistant; consecutive user
|
|
1580
|
+
* messages merge into one. With `since`, only rows stamped after it count (an unreadable stamp still counts). A missing
|
|
1581
|
+
* or unreadable database is 0.
|
|
1582
|
+
*/
|
|
1583
|
+
export function countConversationRounds(dbPath, sid, since = null) {
|
|
1584
|
+
if (!existsSync(dbPath))
|
|
1585
|
+
return 0;
|
|
1586
|
+
let rows;
|
|
1587
|
+
try {
|
|
1588
|
+
const db = openStateDbReadonly(dbPath);
|
|
1589
|
+
try {
|
|
1590
|
+
rows = db.prepare('SELECT role, timestamp FROM messages WHERE session_id = ? ORDER BY timestamp ASC').all(sid);
|
|
1591
|
+
}
|
|
1592
|
+
finally {
|
|
1593
|
+
db.close();
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
catch {
|
|
1597
|
+
return 0;
|
|
1598
|
+
}
|
|
1599
|
+
let rounds = 0;
|
|
1600
|
+
let seenUser = false;
|
|
1601
|
+
let answered = false;
|
|
1602
|
+
for (const row of rows) {
|
|
1603
|
+
if (since !== null) {
|
|
1604
|
+
const ts = stateDbTimestampSeconds(row.timestamp);
|
|
1605
|
+
if (ts !== null && ts <= since)
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
const role = lower(row.role);
|
|
1609
|
+
if (role === 'user') {
|
|
1610
|
+
if (seenUser && answered) {
|
|
1611
|
+
rounds += 1;
|
|
1612
|
+
answered = false;
|
|
1613
|
+
}
|
|
1614
|
+
seenUser = true;
|
|
1615
|
+
}
|
|
1616
|
+
else if (role === 'assistant' && seenUser) {
|
|
1617
|
+
answered = true;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return seenUser && answered ? rounds + 1 : rounds;
|
|
1621
|
+
}
|
|
1622
|
+
/** The content of the session's newest row when that row is a `tool` row (the handoff marker's tail dedupe, TAL-258); else null. */
|
|
1623
|
+
export function stateDbTailToolContent(dbPath, sid) {
|
|
1624
|
+
if (!existsSync(dbPath))
|
|
1625
|
+
return null;
|
|
1626
|
+
try {
|
|
1627
|
+
const db = openStateDbReadonly(dbPath);
|
|
1628
|
+
try {
|
|
1629
|
+
const row = db.prepare('SELECT role, content FROM messages WHERE session_id = ? ORDER BY rowid DESC LIMIT 1').get(sid);
|
|
1630
|
+
const content = row?.role === 'tool' ? decodeStateDbContent(row.content) : null;
|
|
1631
|
+
return typeof content === 'string' ? content : null;
|
|
1632
|
+
}
|
|
1633
|
+
finally {
|
|
1634
|
+
db.close();
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
catch {
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
824
1641
|
//# sourceMappingURL=state-db.js.map
|