From 342222d143bb0d18946920276fab69355594b8e2 Mon Sep 17 00:00:00 2001 From: deepagent-ai Date: Thu, 20 Aug 2026 16:11:32 +0800 Subject: [PATCH] 1.4.7 ### Issue for this PR Closes # ### Type of change - [ ] Bug fix - [ ] New feature - [ ] Refactor / code improvement - [ ] Documentation ### What does this PR do? Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR. **If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!** ### How did you verify your code works? ### Screenshots / recordings _If this is a UI change, please include a screenshot or recording._ ### Checklist - [ ] I have tested my changes locally - [ ] I have not included unrelated changes in this PR _If you do not follow this template your PR will be automatically rejected._ --------- Co-authored-by: deepagent-code --- .github/workflows/deepagent-code.yml | 2 +- .github/workflows/docs-update.yml | 2 +- .github/workflows/test.yml | 20 + .github/workflows/typecheck.yml | 3 +- CHANGELOG.md | 11 + README.md | 2 +- github/README.md | 2 +- github/VERSION | 2 +- packages/app/package.json | 2 +- .../src/components/dialog-console-account.tsx | 105 + .../app/src/components/settings-providers.tsx | 42 +- .../components/settings-v2/import-history.tsx | 67 +- .../src/context/bug407008-projection.test.ts | 435 + .../app/src/context/directory-sync.test.ts | 171 +- packages/app/src/context/directory-sync.ts | 56 +- .../context/global-sync/event-reducer.test.ts | 121 + .../src/context/global-sync/event-reducer.ts | 41 +- packages/app/src/context/layout.tsx | 2 + packages/app/src/context/models.tsx | 69 +- .../src/context/permission-auto-respond.ts | 14 +- packages/app/src/i18n/ar.ts | 16 + packages/app/src/i18n/br.ts | 16 + packages/app/src/i18n/bs.ts | 16 + packages/app/src/i18n/da.ts | 16 + packages/app/src/i18n/de.ts | 16 + packages/app/src/i18n/en.ts | 32 + packages/app/src/i18n/es.ts | 16 + packages/app/src/i18n/fr.ts | 16 + packages/app/src/i18n/ja.ts | 16 + packages/app/src/i18n/ko.ts | 16 + packages/app/src/i18n/no.ts | 16 + packages/app/src/i18n/pl.ts | 16 + packages/app/src/i18n/ru.ts | 16 + packages/app/src/i18n/th.ts | 16 + packages/app/src/i18n/tr.ts | 16 + packages/app/src/i18n/uk.ts | 16 + packages/app/src/i18n/zh.ts | 32 + packages/app/src/i18n/zht.ts | 16 + packages/app/src/pages/session.tsx | 22 +- .../message-timeline-virtualize.test.ts | 172 + .../message-timeline.bug407008.test.ts | 369 + .../session/message-timeline.data.test.ts | 61 +- .../pages/session/message-timeline.data.ts | 107 +- .../src/pages/session/message-timeline.tsx | 94 +- .../src/pages/session/session-side-panel.tsx | 7 + .../src/pages/session/side-panel-stats.tsx | 129 + packages/app/src/utils/message-order.test.ts | 98 + packages/app/src/utils/message-order.ts | 87 + packages/app/src/utils/session-stats.test.ts | 181 + packages/app/src/utils/session-stats.ts | 188 + packages/cli/script/generate.ts | 24 +- packages/cli/src/services/daemon.ts | 71 +- packages/cli/test/daemon.test.ts | 151 + packages/cli/test/fixtures/legacy-entry.ts | 62 + .../migration.sql | 20 + .../snapshot.json | 16233 ++++++++++++++++ packages/core/package.json | 2 +- .../script/release-gate-migration-check.ts | 21 + packages/core/src/agent-gateway.ts | 100 +- .../src/context-federation/provider-owner.ts | 15 +- .../context-federation/reconciliation-sql.ts | 33 + .../src/context-federation/reconciliation.ts | 158 + .../src/context-federation/session-context.ts | 10 +- packages/core/src/database/database.ts | 11 +- packages/core/src/database/migration.gen.ts | 10 + packages/core/src/database/migration.ts | 71 +- ...260815120000_event_delivery_claim_lease.ts | 22 + ...815130000_event_multiprocess_extensions.ts | 41 + ...0260816000000_handoff_admission_receipt.ts | 35 + .../20260816073717_session_facade_activity.ts | 83 + ...000_receipt_active_pack_set_attribution.ts | 24 + ...16081000_session_context_reconciliation.ts | 49 + ...20000_session_intent_confirmation_state.ts | 253 + ...260820000000_remote_compact_persistence.ts | 86 + ...60820120000_session_turn_stage_evidence.ts | 39 + ...000_compaction_continuation_fail_closed.ts | 67 + .../src/deepagent/activity-authority.sql.ts | 38 +- .../core/src/deepagent/activity-authority.ts | 294 +- .../core/src/deepagent/deepagent-event-bus.ts | 332 +- .../core/src/deepagent/deepagent-event-sql.ts | 37 +- packages/core/src/deepagent/domain-pack.ts | 8 + .../src/deepagent/handoff-admission-sql.ts | 49 + .../core/src/deepagent/handoff-admission.ts | 182 + .../core/src/deepagent/knowledge-retriever.ts | 40 +- .../deepagent/learning-lifecycle-trigger.ts | 9 +- packages/core/src/deepagent/lmn-events.ts | 36 + packages/core/src/deepagent/pinned-packs.ts | 39 + .../core/src/deepagent/plan-controller.ts | 14 +- .../core/src/deepagent/profile-builder.ts | 168 + packages/core/src/deepagent/session-state.ts | 9 + packages/core/src/event.ts | 171 + packages/core/src/permission/mutating.ts | 15 + packages/core/src/session/compaction.ts | 5 +- packages/core/src/session/sql.ts | 13 +- packages/core/src/tool/bash.ts | 8 - packages/core/test/agent-gateway.test.ts | 136 + ...continuation-fail-closed-migration.test.ts | 86 + .../core/test/database-capability.test.ts | 27 +- .../core/test/deepagent-event-bus.test.ts | 253 +- .../activity-authority-facade.test.ts | 432 + .../domain-pack-retrieval-isolation.test.ts | 40 +- .../pack-activation-authority.test.ts | 126 + .../core/test/deepagent/pinned-packs.test.ts | 62 + .../test/deepagent/plan-controller.test.ts | 7 + packages/core/test/event-snapshot.test.ts | 72 + packages/core/test/event.test.ts | 67 + packages/core/test/lmn-events.test.ts | 15 + ...mote-compact-persistence-migration.test.ts | 239 + .../session-context-reconciliation.test.ts | 322 + ...ntent-confirmation-state-migration.test.ts | 424 + .../turn-stage-evidence-migration.test.ts | 116 + .../script/live-llm/providers.ts | 165 + .../deepagent-code/script/live-llm/routes.ts | 8 + packages/deepagent-code/script/models-data.ts | 30 +- .../deepagent-code/script/rel-001-drill.ts | 215 + .../script/risk-003-maintenance.ts | 204 + .../src/cli/cmd/github.handler.ts | 2 +- packages/deepagent-code/src/cli/cmd/goal.ts | 108 + .../src/cli/cmd/instance-sdk.ts | 86 + packages/deepagent-code/src/cli/cmd/mcp.ts | 212 +- .../deepagent-code/src/cli/cmd/oversight.ts | 93 + packages/deepagent-code/src/cli/cmd/packs.ts | 64 + packages/deepagent-code/src/cli/cmd/panel.ts | 53 + packages/deepagent-code/src/cli/cmd/review.ts | 50 + packages/deepagent-code/src/cli/cmd/run.ts | 51 +- .../src/cli/cmd/run/permission-mode.ts | 50 + packages/deepagent-code/src/cli/cmd/serve.ts | 64 +- .../deepagent-code/src/cli/cmd/session.ts | 72 +- packages/deepagent-code/src/cli/cmd/wiki.ts | 107 + .../deepagent-code/src/cli/cmd/worktree.ts | 136 + .../src/context-federation/diagnostics.ts | 145 +- .../provider-owner-runtime.ts | 59 +- .../session-context-runtime.ts | 55 +- .../src/deepagent/profile-detector.ts | 176 +- .../deepagent-code/src/effect/app-runtime.ts | 3 + .../src/effect/runtime-flags.ts | 44 +- packages/deepagent-code/src/index.ts | 14 + .../src/legacy-event-canonicalizer-runtime.ts | 71 + .../src/location-index/runtime.ts | 7 +- .../deepagent-code/src/permission/index.ts | 205 +- .../deepagent-code/src/provider/transform.ts | 42 + .../routes/instance/httpapi/groups/session.ts | 81 + .../instance/httpapi/handlers/deepagent.ts | 136 +- .../instance/httpapi/handlers/session.ts | 46 +- .../server/routes/instance/httpapi/public.ts | 5 + .../src/session/__tests__/snapshot.test.ts | 367 + .../src/session/activity-crash-test.ts | 4 + .../src/session/agent-handoff-consumer.ts | 112 +- .../src/session/compaction-sql.ts | 27 + .../deepagent-code/src/session/compaction.ts | 395 +- .../src/session/diff-artifact.ts | 286 +- .../src/session/durable-executor-lock.ts | 35 +- .../src/session/event-dispatcher.ts | 14 +- .../src/session/facade-activity.ts | 986 + .../src/session/goal-manager.ts | 6 +- .../src/session/goal-tick-consumer.ts | 8 +- packages/deepagent-code/src/session/llm.ts | 3 + .../src/session/llm/native-request.ts | 23 +- .../src/session/llm/native-runtime.ts | 9 +- .../deepagent-code/src/session/llm/request.ts | 88 +- .../src/session/llm/tool-pairing.ts | 157 + .../deepagent-code/src/session/processor.ts | 67 +- .../src/session/prompt-intent.ts | 166 +- packages/deepagent-code/src/session/prompt.ts | 601 +- .../src/session/remote-compact.ts | 245 + packages/deepagent-code/src/session/retry.ts | 58 +- .../deepagent-code/src/session/session.ts | 52 +- .../deepagent-code/src/session/snapshot.ts | 384 + packages/deepagent-code/src/session/steer.ts | 6 +- .../src/session/tool-request-receipt.sql.ts | 3 + .../src/session/turn-deadline-watchdog.ts | 144 + .../src/session/turn-stage-evidence.sql.ts | 27 + .../src/session/turn-stage-evidence.ts | 111 + .../src/session/v4-event-runtime.ts | 4 + packages/deepagent-code/src/snapshot/index.ts | 114 +- .../src/tool/activity_facade.ts | 292 + packages/deepagent-code/src/tool/registry.ts | 49 + packages/deepagent-code/src/tool/task-run.ts | 130 + packages/deepagent-code/src/tool/task.ts | 58 +- packages/deepagent-code/src/tool/truncate.ts | 18 +- .../src/wiki/event-driven-archiver.ts | 8 +- .../src/wiki/session-archive.ts | 53 +- .../deepagent-code/src/wiki/wiki-events.ts | 103 + .../deepagent-code/src/wiki/wiki-service.ts | 53 +- packages/deepagent-code/src/worktree/index.ts | 62 + .../test/agent/pr-collaboration.test.ts | 10 +- .../test/cli/goal-worktree.test.ts | 66 + .../__snapshots__/help-snapshots.test.ts.snap | 467 +- .../test/cli/help/help-snapshots.test.ts | 24 + .../test/cli/mcp-catalog.test.ts | 51 + .../deepagent-code/test/cli/mcp-edit.test.ts | 137 + .../test/cli/mcp-remove.test.ts | 88 + .../test/cli/query-commands.test.ts | 86 + .../test/cli/run/permission-mode.test.ts | 83 + .../test/cli/run/run-process.test.ts | 37 + .../test/cli/serve/serve-process.test.ts | 62 + .../test/cli/session-rename-archive.test.ts | 74 + .../context-federation/diagnostics.test.ts | 198 +- .../provider-owner-runtime.test.ts | 209 +- .../test/control-plane/workspace.test.ts | 230 +- .../deepagent/learning-lifecycle-e2e.test.ts | 468 + .../test/effect/runtime-flags.test.ts | 12 - ...legacy-event-canonicalizer-runtime.test.ts | 152 + .../deepagent-code/test/mcp/headers.test.ts | 8 +- .../deepagent-code/test/mcp/lifecycle.test.ts | 9 +- .../test/mcp/oauth-auto-connect.test.ts | 8 +- .../test/mcp/oauth-browser.test.ts | 8 +- .../test/provider/transform.test.ts | 83 +- .../test/script/models-data.test.ts | 14 + .../test/script/release-evidence.test.ts | 188 + .../test/script/run-live-llm-all.test.ts | 14 + .../test/server/httpapi-mdns.test.ts | 8 +- .../test/server/httpapi-packs-events.test.ts | 259 + .../server/httpapi-provider-recovery.test.ts | 245 +- .../test/server/httpapi-sdk.test.ts | 7 +- .../server/httpapi-workspace-routing.test.ts | 3 +- .../server/session-diff-missing-patch.test.ts | 12 +- .../session/agent-handoff-consumer.test.ts | 110 +- .../session/bug-008-activity-leak.test.ts | 565 + .../test/session/bug-407-008-runtime.test.ts | 1745 ++ .../test/session/compacted-fork.test.ts | 7 +- .../test/session/compaction.test.ts | 591 +- .../test/session/context-window.test.ts | 5 +- .../test/session/diff-artifact.test.ts | 189 +- .../test/session/facade-activity.test.ts | 491 + .../test/session/goal-driver.test.ts | 28 + .../test/session/goal-tick-consumer.test.ts | 62 +- .../test/session/incident-regression.test.ts | 1607 ++ .../legacy-provider-resolution.test.ts | 6 +- .../test/session/llm-native.test.ts | 121 +- .../test/session/llm/tool-pairing.test.ts | 322 + .../message-update-event-bytes.test.ts | 126 + .../test/session/messages-pagination.test.ts | 5 +- .../session/multi-process-takeover.test.ts | 359 + .../test/session/processor-effect.test.ts | 15 +- .../test/session/prompt-intent.test.ts | 4 +- .../test/session/prompt.test.ts | 422 +- .../session/provider-receipt-recovery.test.ts | 6 +- .../test/session/remote-compact.test.ts | 374 + .../deepagent-code/test/session/retry.test.ts | 88 + .../test/session/revert-compact.test.ts | 4 + .../test/session/snapshot-tool-race.test.ts | 28 +- .../deepagent-code/test/session/steer.test.ts | 24 +- .../test/session/task-fork.test.ts | 7 +- .../test/session/turn-stage-evidence.test.ts | 452 + .../test/session/v4-integration.test.ts | 3 + .../test/share/share-next.test.ts | 4 + .../snapshot-degraded-attribution.test.ts | 63 + .../test/snapshot/snapshot.test.ts | 36 + .../test/tool/degraded-text-recovery.test.ts | 312 + .../test/tool/output-degeneration.test.ts | 177 + .../test/tool/pr-finalize.test.ts | 4 + .../deepagent-code/test/tool/registry.test.ts | 92 + .../test/tool/task-read.test.ts | 4 + .../test/tool/task-recovery.test.ts | 4 + .../test/tool/task-status.test.ts | 4 + .../test/tool/task-takeover.test.ts | 4 + .../test/wiki/wiki-events.test.ts | 245 + packages/desktop/package.json | 2 +- packages/llm/src/protocols/index.ts | 1 + .../protocols/openai-compatible-responses.ts | 29 + .../llm/src/protocols/openai-responses.ts | 199 +- .../providers/openai-compatible-profile.ts | 32 +- .../llm/src/providers/openai-compatible.ts | 19 +- packages/llm/src/route/executor.ts | 117 +- packages/llm/src/schema/errors.ts | 20 +- packages/llm/src/schema/messages.ts | 10 +- packages/llm/test/executor.test.ts | 154 +- packages/llm/test/exports.test.ts | 5 +- .../test/provider/bug407008-reasoning.test.ts | 456 + .../openai-compatible-responses.test.ts | 145 + .../provider/openai-responses-compact.test.ts | 203 + .../test/provider/openai-responses.test.ts | 83 + packages/sdk/js/src/gen/sdk.gen.ts | 95 + packages/sdk/js/src/gen/types.gen.ts | 205 +- packages/sdk/js/src/v2/gen/sdk.gen.ts | 95 + packages/sdk/js/src/v2/gen/types.gen.ts | 205 +- packages/ui/src/components/file.tsx | 5 + .../src/components/message-part-chunk.test.ts | 97 + .../ui/src/components/message-part-chunk.ts | 91 + packages/ui/src/components/message-part.tsx | 76 +- packages/web/src/content/docs/ar/github.mdx | 10 +- packages/web/src/content/docs/bs/github.mdx | 10 +- packages/web/src/content/docs/da/github.mdx | 10 +- packages/web/src/content/docs/de/github.mdx | 10 +- packages/web/src/content/docs/es/github.mdx | 10 +- packages/web/src/content/docs/fr/github.mdx | 10 +- packages/web/src/content/docs/github.mdx | 10 +- packages/web/src/content/docs/it/github.mdx | 10 +- packages/web/src/content/docs/ja/github.mdx | 10 +- packages/web/src/content/docs/ko/github.mdx | 10 +- packages/web/src/content/docs/nb/github.mdx | 10 +- packages/web/src/content/docs/pl/github.mdx | 10 +- .../web/src/content/docs/pt-br/github.mdx | 10 +- packages/web/src/content/docs/ru/github.mdx | 10 +- packages/web/src/content/docs/th/github.mdx | 10 +- packages/web/src/content/docs/tr/github.mdx | 10 +- .../web/src/content/docs/zh-cn/github.mdx | 10 +- .../web/src/content/docs/zh-tw/github.mdx | 10 +- release-evidence/README.md | 77 + release-evidence/manifest.schema.json | 115 + .../release-gate-20260819/manifest.json | 52 + .../release-gate-20260820/manifest.json | 51 + script/check-test-fixtures.ts | 167 + script/live-llm-key-from-auth.ts | 33 + script/release-evidence.ts | 534 + script/run-live-llm-all.ts | 86 +- 307 files changed, 47201 insertions(+), 1246 deletions(-) create mode 100644 packages/app/src/components/dialog-console-account.tsx create mode 100644 packages/app/src/context/bug407008-projection.test.ts create mode 100644 packages/app/src/pages/session/message-timeline-virtualize.test.ts create mode 100644 packages/app/src/pages/session/message-timeline.bug407008.test.ts create mode 100644 packages/app/src/pages/session/side-panel-stats.tsx create mode 100644 packages/app/src/utils/message-order.test.ts create mode 100644 packages/app/src/utils/message-order.ts create mode 100644 packages/app/src/utils/session-stats.test.ts create mode 100644 packages/app/src/utils/session-stats.ts create mode 100644 packages/cli/test/daemon.test.ts create mode 100644 packages/cli/test/fixtures/legacy-entry.ts create mode 100644 packages/core/migration/20260816073717_session_facade_activity/migration.sql create mode 100644 packages/core/migration/20260816073717_session_facade_activity/snapshot.json create mode 100644 packages/core/script/release-gate-migration-check.ts create mode 100644 packages/core/src/context-federation/reconciliation-sql.ts create mode 100644 packages/core/src/context-federation/reconciliation.ts create mode 100644 packages/core/src/database/migration/20260815120000_event_delivery_claim_lease.ts create mode 100644 packages/core/src/database/migration/20260815130000_event_multiprocess_extensions.ts create mode 100644 packages/core/src/database/migration/20260816000000_handoff_admission_receipt.ts create mode 100644 packages/core/src/database/migration/20260816073717_session_facade_activity.ts create mode 100644 packages/core/src/database/migration/20260816080000_receipt_active_pack_set_attribution.ts create mode 100644 packages/core/src/database/migration/20260816081000_session_context_reconciliation.ts create mode 100644 packages/core/src/database/migration/20260819120000_session_intent_confirmation_state.ts create mode 100644 packages/core/src/database/migration/20260820000000_remote_compact_persistence.ts create mode 100644 packages/core/src/database/migration/20260820120000_session_turn_stage_evidence.ts create mode 100644 packages/core/src/database/migration/20260820130000_compaction_continuation_fail_closed.ts create mode 100644 packages/core/src/deepagent/handoff-admission-sql.ts create mode 100644 packages/core/src/deepagent/handoff-admission.ts create mode 100644 packages/core/src/deepagent/pinned-packs.ts create mode 100644 packages/core/src/deepagent/profile-builder.ts create mode 100644 packages/core/src/permission/mutating.ts create mode 100644 packages/core/test/compaction-continuation-fail-closed-migration.test.ts create mode 100644 packages/core/test/deepagent/activity-authority-facade.test.ts create mode 100644 packages/core/test/deepagent/pack-activation-authority.test.ts create mode 100644 packages/core/test/deepagent/pinned-packs.test.ts create mode 100644 packages/core/test/remote-compact-persistence-migration.test.ts create mode 100644 packages/core/test/session-context-reconciliation.test.ts create mode 100644 packages/core/test/session-intent-confirmation-state-migration.test.ts create mode 100644 packages/core/test/turn-stage-evidence-migration.test.ts create mode 100644 packages/deepagent-code/script/live-llm/providers.ts create mode 100644 packages/deepagent-code/script/rel-001-drill.ts create mode 100644 packages/deepagent-code/script/risk-003-maintenance.ts create mode 100644 packages/deepagent-code/src/cli/cmd/goal.ts create mode 100644 packages/deepagent-code/src/cli/cmd/instance-sdk.ts create mode 100644 packages/deepagent-code/src/cli/cmd/oversight.ts create mode 100644 packages/deepagent-code/src/cli/cmd/packs.ts create mode 100644 packages/deepagent-code/src/cli/cmd/panel.ts create mode 100644 packages/deepagent-code/src/cli/cmd/review.ts create mode 100644 packages/deepagent-code/src/cli/cmd/run/permission-mode.ts create mode 100644 packages/deepagent-code/src/cli/cmd/wiki.ts create mode 100644 packages/deepagent-code/src/cli/cmd/worktree.ts create mode 100644 packages/deepagent-code/src/legacy-event-canonicalizer-runtime.ts create mode 100644 packages/deepagent-code/src/session/__tests__/snapshot.test.ts create mode 100644 packages/deepagent-code/src/session/facade-activity.ts create mode 100644 packages/deepagent-code/src/session/llm/tool-pairing.ts create mode 100644 packages/deepagent-code/src/session/remote-compact.ts create mode 100644 packages/deepagent-code/src/session/snapshot.ts create mode 100644 packages/deepagent-code/src/session/turn-deadline-watchdog.ts create mode 100644 packages/deepagent-code/src/session/turn-stage-evidence.sql.ts create mode 100644 packages/deepagent-code/src/session/turn-stage-evidence.ts create mode 100644 packages/deepagent-code/src/tool/activity_facade.ts create mode 100644 packages/deepagent-code/src/wiki/wiki-events.ts create mode 100644 packages/deepagent-code/test/cli/goal-worktree.test.ts create mode 100644 packages/deepagent-code/test/cli/mcp-catalog.test.ts create mode 100644 packages/deepagent-code/test/cli/mcp-edit.test.ts create mode 100644 packages/deepagent-code/test/cli/mcp-remove.test.ts create mode 100644 packages/deepagent-code/test/cli/query-commands.test.ts create mode 100644 packages/deepagent-code/test/cli/run/permission-mode.test.ts create mode 100644 packages/deepagent-code/test/cli/session-rename-archive.test.ts create mode 100644 packages/deepagent-code/test/deepagent/learning-lifecycle-e2e.test.ts create mode 100644 packages/deepagent-code/test/legacy-event-canonicalizer-runtime.test.ts create mode 100644 packages/deepagent-code/test/script/release-evidence.test.ts create mode 100644 packages/deepagent-code/test/server/httpapi-packs-events.test.ts create mode 100644 packages/deepagent-code/test/session/bug-008-activity-leak.test.ts create mode 100644 packages/deepagent-code/test/session/bug-407-008-runtime.test.ts create mode 100644 packages/deepagent-code/test/session/facade-activity.test.ts create mode 100644 packages/deepagent-code/test/session/incident-regression.test.ts create mode 100644 packages/deepagent-code/test/session/llm/tool-pairing.test.ts create mode 100644 packages/deepagent-code/test/session/message-update-event-bytes.test.ts create mode 100644 packages/deepagent-code/test/session/multi-process-takeover.test.ts create mode 100644 packages/deepagent-code/test/session/remote-compact.test.ts create mode 100644 packages/deepagent-code/test/session/turn-stage-evidence.test.ts create mode 100644 packages/deepagent-code/test/snapshot/snapshot-degraded-attribution.test.ts create mode 100644 packages/deepagent-code/test/tool/degraded-text-recovery.test.ts create mode 100644 packages/deepagent-code/test/tool/output-degeneration.test.ts create mode 100644 packages/deepagent-code/test/wiki/wiki-events.test.ts create mode 100644 packages/llm/src/protocols/openai-compatible-responses.ts create mode 100644 packages/llm/test/provider/bug407008-reasoning.test.ts create mode 100644 packages/llm/test/provider/openai-compatible-responses.test.ts create mode 100644 packages/llm/test/provider/openai-responses-compact.test.ts create mode 100644 packages/ui/src/components/message-part-chunk.test.ts create mode 100644 packages/ui/src/components/message-part-chunk.ts create mode 100644 release-evidence/README.md create mode 100644 release-evidence/manifest.schema.json create mode 100644 release-evidence/v4.1/6fa05579a27d7db372da502d112efaab23353d15/release-gate-20260819/manifest.json create mode 100644 release-evidence/v4.1/9bad421e500b20cf6948e3b250203722afafbdb0/release-gate-20260820/manifest.json create mode 100644 script/check-test-fixtures.ts create mode 100644 script/live-llm-key-from-auth.ts create mode 100644 script/release-evidence.ts diff --git a/.github/workflows/deepagent-code.yml b/.github/workflows/deepagent-code.yml index 9a117e8a..0eab6d2e 100644 --- a/.github/workflows/deepagent-code.yml +++ b/.github/workflows/deepagent-code.yml @@ -26,7 +26,7 @@ jobs: - uses: ./.github/actions/setup-bun - name: Run deepagent-code - uses: deepagent-ltd/deepagent-code/github@github-v1.4.5 + uses: deepagent-ltd/deepagent-code/github@github-v1.4.7 env: DEEPAGENT_CODE_API_KEY: ${{ secrets.DEEPAGENT_CODE_API_KEY }} DEEPAGENT_CODE_PERMISSION: '{"bash": "deny"}' diff --git a/.github/workflows/docs-update.yml b/.github/workflows/docs-update.yml index d503a9f2..b707db2a 100644 --- a/.github/workflows/docs-update.yml +++ b/.github/workflows/docs-update.yml @@ -43,7 +43,7 @@ jobs: - name: Run deepagent-code if: steps.commits.outputs.has_commits == 'true' - uses: deepagent-ltd/deepagent-code/github@github-v1.4.5 + uses: deepagent-ltd/deepagent-code/github@github-v1.4.7 env: DEEPAGENT_CODE_API_KEY: ${{ secrets.DEEPAGENT_CODE_API_KEY }} with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3c55bd09..787ade76 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,26 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: + test-fixture-gate: + name: test-fixture-gate + # QUAL-003: forbid test fixtures from bypassing the durable session state + # machine via direct inserts into authority tables. Pure grep gate, no deps. + runs-on: blacksmith-4vcpu-ubuntu-2404 + defaults: + run: + shell: bash + steps: + - name: Checkout repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Run test fixture gate + run: bun script/check-test-fixtures.ts + unit: name: unit (${{ matrix.settings.name }}) strategy: diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml index fc9a5279..0e5c3b3b 100644 --- a/.github/workflows/typecheck.yml +++ b/.github/workflows/typecheck.yml @@ -3,8 +3,9 @@ name: typecheck on: push: branches: [dev] + # Gate every PR regardless of target branch: the typecheck baseline is easily polluted by stale + # worktrees/generated files, so keep it required and branch-agnostic. pull_request: - branches: [dev] workflow_dispatch: jobs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b906cc..49ba7836 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Desktop 1.4.7 / DeepAgent Core V4.0.8 - Release hardening and data governance + +- Recover the two incident sessions through the formal abandoned path with a zero-provider-invocation oracle and fork smoke on production-snapshot copies; fence pre-successor binaries via a protocol-3 database capability so older builds refuse to open databases carrying successor ownership. +- Fix the desktop startup crash in the session_intent rebuild migration on SQLite 3.51 (node runtime) and retry transient schema-lock failures in the migration runner. +- Stop the infinite compaction-continuation recovery loop on startup: recovery failures now land fail-closed (pending → failed) through the widened continuation state machine, serialized per session, with warning-level telemetry. +- Add durable turn stage evidence and a pre-dispatch watchdog for hung activities; order app timelines by (time, id) so wrapped opaque IDs never reorder messages. +- Persist encrypted remote-compaction content per session and commit remote compaction runs with an opaque blob pointer instead of a text summary, exempt from information-hole validation. +- Land RISK-003 data governance: runtime diff-artifact capture (staged behind a flag), a legacy event canonicalizer daemon, portable aggregate snapshot bundles, and a gate-ordered maintenance drill that reclaimed hundreds of MiB on production-snapshot copies (33,168 events deleted, VACUUM 5075 → 4311 MiB). +- Add the release-evidence manifest infrastructure and archive the 2026-08-19/20 release gates; lock REL-002 wire-budget and discovery-budget regressions; retire the legacy activity authority in favor of the durable path. +- Align Desktop 1.4.7, DeepAgent Core V4.0.8, and GitHub Action `github-v1.4.7` across package metadata, updater tags, and workflow references. + ## Desktop 1.4.5 / DeepAgent Core V4.0.6 - Bounded planning and runtime hardening - Make compaction durable through explicit run/attempt lifecycle records and Prompt Epoch authority, with bounded text-only summary retries and typed terminal failures. diff --git a/README.md b/README.md index 5572d09f..c96e8638 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@

English | - 简体中文 | + 中文 | Enterprise

diff --git a/github/README.md b/github/README.md index 949c933d..95d09412 100644 --- a/github/README.md +++ b/github/README.md @@ -90,7 +90,7 @@ This will walk you through installing the GitHub app, creating the workflow, and persist-credentials: false - name: Run deepagent-code - uses: deepagent-ltd/deepagent-code/github@github-v1.4.5 + uses: deepagent-ltd/deepagent-code/github@github-v1.4.7 env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/github/VERSION b/github/VERSION index e516bb9d..be05bba9 100644 --- a/github/VERSION +++ b/github/VERSION @@ -1 +1 @@ -1.4.5 +1.4.7 diff --git a/packages/app/package.json b/packages/app/package.json index b143030a..cea1b8d1 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "@deepagent-code/app", - "version": "1.4.6", + "version": "1.4.7", "description": "", "type": "module", "exports": { diff --git a/packages/app/src/components/dialog-console-account.tsx b/packages/app/src/components/dialog-console-account.tsx new file mode 100644 index 00000000..ed101149 --- /dev/null +++ b/packages/app/src/components/dialog-console-account.tsx @@ -0,0 +1,105 @@ +import { createResource, createSignal, type Component, For, Show } from "solid-js" +import { useDialog } from "@deepagent-code/ui/context/dialog" +import { Dialog } from "@deepagent-code/ui/dialog" +import { Spinner } from "@deepagent-code/ui/spinner" +import { showToast } from "@/utils/toast" +import { useLanguage } from "@/context/language" +import { useServerSDK } from "@/context/server-sdk" +import { useServerSync } from "@/context/server-sync" + +// PARITY-004 / GUI 批 — Console 账号接线:消费 /experimental/console/orgs 与 /switch, +// 列出可切换的 Console 组织并切换活跃组织(server-side truth,见 worklist GUI 批)。 +type Org = { + accountID: string + accountEmail: string + accountUrl: string + orgID: string + orgName: string + active: boolean +} + +export const DialogConsoleAccount: Component = () => { + const dialog = useDialog() + const language = useLanguage() + const serverSDK = useServerSDK() + const serverSync = useServerSync() + const [switching, setSwitching] = createSignal() + + const [orgs] = createResource(async () => { + const result = await serverSDK.client.experimental.console.listOrgs(undefined, { throwOnError: true }) + return (result.data?.orgs ?? []) as Org[] + }) + + const switchOrg = async (org: Org) => { + if (switching()) return + setSwitching(`${org.accountID}:${org.orgID}`) + await serverSDK.client.experimental.console + .switchOrg({ accountID: org.accountID, orgID: org.orgID }, { throwOnError: true }) + .then(() => { + serverSync.refreshProviders() + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("settings.providers.console.switch.toast.title"), + description: language.t("settings.providers.console.switch.toast.description", { org: org.orgName }), + }) + dialog.close() + }) + .catch((err: unknown) => { + const message = err instanceof Error ? err.message : String(err) + showToast({ title: language.t("common.requestFailed"), description: message }) + }) + .finally(() => setSwitching(undefined)) + } + + return ( + + + + + } + > + 0} + fallback={ +
+ {language.t("settings.providers.console.empty")} +
+ } + > +
+ + {(org) => { + const key = `${org.accountID}:${org.orgID}` + return ( + + ) + }} + +
+
+
+
+ ) +} diff --git a/packages/app/src/components/settings-providers.tsx b/packages/app/src/components/settings-providers.tsx index 07f5da31..32fee603 100644 --- a/packages/app/src/components/settings-providers.tsx +++ b/packages/app/src/components/settings-providers.tsx @@ -4,7 +4,7 @@ import { ProviderIcon } from "@deepagent-code/ui/provider-icon" import { Tag } from "@deepagent-code/ui/tag" import { showToast } from "@/utils/toast" import { popularProviders, useProviders } from "@/hooks/use-providers" -import { createMemo, type Component, For, Show } from "solid-js" +import { createMemo, createResource, type Component, For, Show } from "solid-js" import { createStore } from "solid-js/store" import { useLanguage } from "@/context/language" import { useServerSDK } from "@/context/server-sdk" @@ -12,6 +12,7 @@ import { useServerSync } from "@/context/server-sync" import { DialogConnectProvider } from "./dialog-connect-provider" import { DialogSelectProvider } from "./dialog-select-provider" import { DialogCustomProvider } from "./dialog-custom-provider" +import { DialogConsoleAccount } from "./dialog-console-account" import { SettingsList } from "./settings-list" import { SettingsServerPicker, SettingsServerScope } from "./settings-server-picker" import { canRefreshProviderModels } from "./provider-model-refresh" @@ -50,6 +51,12 @@ const SettingsProvidersContent: Component = () => { const providers = useProviders() const [refreshing, setRefreshing] = createStore>({}) + // PARITY-004 / GUI 批 — Console 账号:消费 /experimental/console,显示活跃组织与可切换数量。 + const [consoleState] = createResource(async () => { + const result = await serverSDK.client.experimental.console.get().catch(() => undefined) + return result?.data + }) + const isConfigCustom = (providerID: string) => { const provider = serverSync.data.config.provider?.[providerID] if (!provider) return false @@ -180,6 +187,39 @@ const SettingsProvidersContent: Component = () => {
+ 0 || (consoleState()?.consoleManagedProviders.length ?? 0) > 0} + > +
+

+ {language.t("settings.providers.section.consoleAccount")} +

+ +
+
+ + {consoleState()?.activeOrgName ?? language.t("settings.providers.console.noOrg")} + + + {language.t("settings.providers.console.managedCount", { + count: consoleState()?.consoleManagedProviders.length ?? 0, + })} + +
+ +
+
+
+
+

{language.t("settings.providers.section.connected")}

diff --git a/packages/app/src/components/settings-v2/import-history.tsx b/packages/app/src/components/settings-v2/import-history.tsx index a351bfe7..759c16f6 100644 --- a/packages/app/src/components/settings-v2/import-history.tsx +++ b/packages/app/src/components/settings-v2/import-history.tsx @@ -10,7 +10,7 @@ import { SettingsListV2 } from "./parts/list" import { SettingsRowV2 } from "./parts/row" import "./settings-v2.css" -type SourceFormat = "codex" | "claude" +type SourceFormat = "codex" | "claude" | "deepagent" type SourceMode = SourceFormat | "custom" type Scope = "session" | "memory" | "skill" type ProgressEvent = @@ -90,6 +90,7 @@ export const ImportSection: Component = () => { const [dryRun, setDryRun] = createSignal(false) const [copyLiveDb, setCopyLiveDb] = createSignal(true) const [cwdFilter, setCwdFilter] = createSignal("") + const [snapshotFile, setSnapshotFile] = createSignal(undefined) const [running, setRunning] = createSignal(false) const [logs, setLogs] = createSignal([]) const [summary, setSummary] = createSignal("") @@ -113,6 +114,7 @@ export const ImportSection: Component = () => { const modeOptions = createMemo(() => [ { value: "codex" as const, label: t("settings.import.preset.codex", "Codex") }, { value: "claude" as const, label: t("settings.import.preset.claude", "Claude Code") }, + { value: "deepagent" as const, label: t("settings.import.preset.deepagent", "DeepAgent Code (snapshot)") }, { value: "custom" as const, label: t("settings.import.preset.custom", "Custom") }, ]) const formatOptions = createMemo(() => [ @@ -125,6 +127,42 @@ export const ImportSection: Component = () => { { id: "skill", icon: "bookmark" }, ] + // DeepAgent Code snapshot import: read the chosen bundle file and POST it to + // /session/import-snapshot (server rebuilds a fresh, continuable session). + const runSnapshotImport = async (h: HttpBase | undefined) => { + try { + const file = snapshotFile() + if (!file) { + push(` [error] ${t("settings.import.snapshot.noFile", "Choose a snapshot bundle file first")}`) + return + } + const bundle = await file.text() + const res = await fetch(`${h?.url ?? ""}/session/import-snapshot`, { + method: "POST", + headers: { "content-type": "application/json", ...authHeader(h) }, + body: JSON.stringify({ bundle }), + signal: abort?.signal, + }) + if (!res.ok) { + const text = await res.text().catch(() => res.statusText) + push(` [error] HTTP ${res.status}: ${text.slice(0, 200)}`) + return + } + const result = (await res.json().catch(() => undefined)) as + | { sessionID?: string; messages?: number; parts?: number } + | undefined + push( + ` [session] imported ${result?.sessionID?.slice(0, 16) ?? ""}… · ${result?.messages ?? 0} messages · ${result?.parts ?? 0} parts`, + ) + setSummary(t("settings.import.snapshot.done", "Snapshot imported as a new continuable session")) + } catch (err) { + const message = err instanceof Error ? err.message : String(err) + push(` [error] ${message}`) + } finally { + setRunning(false) + } + } + const run = async () => { if (running()) return setLogs([]) @@ -134,6 +172,10 @@ export const ImportSection: Component = () => { const h = http() const src = format() push(`→ ${src}${mode() === "custom" ? ` (custom: ${sourcePath() || "default"})` : ` (default)`} · scopes: ${scopes().join(",")}${dryRun() ? " · dry-run" : ""}`) + if (src === "deepagent") { + await runSnapshotImport(h) + return + } try { await streamImport( h?.url ?? "", @@ -218,6 +260,29 @@ export const ImportSection: Component = () => { /> + + +
+ setSnapshotFile(e.currentTarget.files?.[0])} + /> + + {(file) => {file().name}} + +
+
+
+
diff --git a/packages/app/src/context/bug407008-projection.test.ts b/packages/app/src/context/bug407008-projection.test.ts new file mode 100644 index 00000000..486f9425 --- /dev/null +++ b/packages/app/src/context/bug407008-projection.test.ts @@ -0,0 +1,435 @@ +/** + * BUG-407-008 — spec §7.2 App projection deterministic regression tests (scenarios 20-25, + * sync/reducer layer). Harness style mirrors ./global-sync/event-reducer.test.ts and + * ./directory-sync.test.ts. + * + * Covered layers: + * - `mergeMessage` / `applyDirectoryEvent` (packages/app/src/context/global-sync/event-reducer.ts) + * — Fix-D server-owned marker monotonicity + conflict → refetchSession. + * - `runInflight` (packages/app/src/context/directory-sync.ts) — keyed trailing forced sync. + * - `createDirSyncContext` pagination — deterministic convergence across page order/refresh. + * + * Implementation note: the working tree carries a BUG-005 change that classifies a same-activity + * revision mismatch as staleness (keep the higher revision, no conflict, no forced reload). Tests + * below pin that actual semantics and annotate where it differs from earlier behavior. + */ +import { describe, expect, test } from "bun:test" +import { createRoot } from "solid-js" +import { createStore, type SetStoreFunction, type Store } from "solid-js/store" +import type { Message } from "@deepagent-code/sdk/v2/client" +import { ServerScope } from "@/utils/server-scope" +import type { State } from "./global-sync/types" +import { applyDirectoryEvent } from "./global-sync/event-reducer" +import { createDirSyncContext, runInflight } from "./directory-sync" + +type AssistantMessage = Extract + +const baseState = (input: Partial = {}) => + ({ + status: "complete", + agent: [], + command: [], + project: "", + projectMeta: undefined, + icon: undefined, + provider: {} as State["provider"], + config: {} as State["config"], + path: { directory: "/tmp" } as State["path"], + session: [], + sessionTotal: 0, + session_status: {}, + session_diff: {}, + todo: {}, + permission: {}, + question: {}, + mcp: {}, + lsp: [], + vcs: undefined, + limit: 10, + message: {}, + part: {}, + part_text_accum_delta: {}, + ...input, + }) as State + +const assistantMessage = ( + id: string, + sessionID: string, + activityProgress?: AssistantMessage["activityProgress"], +) => + ({ + id, + sessionID, + role: "assistant", + parentID: "msg_user", + time: { created: 1 }, + modelID: "gpt", + providerID: "openai", + mode: "build", + agent: "assistant", + path: { cwd: "/tmp", root: "/tmp" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + activityProgress, + }) as AssistantMessage + +const INCIDENT_ACTIVITY = "a6d06b2a82ef234ab9dc71e3fd940292a21b34f4732f28aa19ba8416c4c5e5a9" + +const applyMessageUpdated = ( + store: Store, + setStore: SetStoreFunction, + info: Message, + refetchSession?: (sessionID: string) => void, +) => + applyDirectoryEvent({ + event: { type: "message.updated", properties: { info } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + ...(refetchSession ? { refetchSession } : {}), + }) + +const firstAssistant = (store: State, sessionID: string) => + store.message[sessionID]?.[0] as AssistantMessage | undefined + +describe("BUG-407-008 §7.2 App projection — reducer monotonicity (scenarios 20-22)", () => { + test("20: revision 8 arrives via SSE first, revision 9 after — latest revision 9 is established", () => { + const sessionID = "ses_0149b8afffffWlu80cVGdzFI9s" + const [store, setStore] = createStore(baseState()) + + // revision 8's owning assistant message arrives first over the realtime stream. + applyMessageUpdated( + store, + setStore, + assistantMessage("msg_incident_r8", sessionID, { + activityID: INCIDENT_ACTIVITY, + revision: 8, + state: "progress", + }), + ) + // revision 9's owning assistant message arrives afterwards. + applyMessageUpdated( + store, + setStore, + assistantMessage("msg_incident_r9", sessionID, { + activityID: INCIDENT_ACTIVITY, + revision: 9, + state: "progress", + }), + ) + + // Both owning messages coexist in the store with their server-owned markers intact; the + // "only revision 9 is visible" contract is a timeline-layer selection (Fix-E). Fix-E was + // removed in commit 1ae96499 — see message-timeline.bug407008.test.ts scenario 18 for the + // pinned actual timeline behavior. At the store layer the latest revision is well-defined: + expect(store.message[sessionID]?.map((m) => m.id)).toEqual(["msg_incident_r8", "msg_incident_r9"]) + expect((store.message[sessionID]?.[0] as AssistantMessage).activityProgress?.revision).toBe(8) + expect((store.message[sessionID]?.[1] as AssistantMessage).activityProgress?.revision).toBe(9) + }) + + test("21: revision 9 first, out-of-order revision 8 after — no regression to revision 8", () => { + const sessionID = "ses_1" + const current = assistantMessage("msg_assistant", sessionID, { + activityID: INCIDENT_ACTIVITY, + revision: 9, + state: "progress", + }) + const [store, setStore] = createStore(baseState({ message: { [sessionID]: [current] } })) + const refreshed: string[] = [] + + // A stale event/page replays the SAME owning message with the older revision 8. + applyMessageUpdated( + store, + setStore, + assistantMessage(current.id, sessionID, { + activityID: INCIDENT_ACTIVITY, + revision: 8, + state: "progress", + }), + (id) => refreshed.push(id), + ) + + // The store never regresses to revision 8 ... + expect(firstAssistant(store, sessionID)?.activityProgress?.revision).toBe(9) + expect(firstAssistant(store, sessionID)?.activityProgress?.activityID).toBe(INCIDENT_ACTIVITY) + // ... and, per the working-tree BUG-005 semantics, a same-activity revision mismatch is + // treated as staleness: it keeps the higher revision WITHOUT escalating to a forced session + // reload. (Before BUG-005 this same event was a conflict that triggered refetchSession.) + expect(refreshed).toEqual([]) + }) + + test("22: after a terminal marker, marker-less or stale progress events cannot clear or downgrade it", () => { + const sessionID = "ses_1" + const terminal = assistantMessage("msg_assistant", sessionID, { + activityID: INCIDENT_ACTIVITY, + revision: 10, + state: "interrupted", + terminalReason: "user_abort", + }) + const [store, setStore] = createStore(baseState({ message: { [sessionID]: [terminal] } })) + const refreshed: string[] = [] + const refetch = (id: string) => refreshed.push(id) + + // (a) A late ordinary update WITHOUT any marker must not clear the terminal marker. + applyMessageUpdated(store, setStore, { ...terminal, activityProgress: undefined }, refetch) + expect(firstAssistant(store, sessionID)?.activityProgress).toEqual(terminal.activityProgress) + + // (b) A stale event replaying the same revision as non-terminal `progress` must not downgrade. + applyMessageUpdated( + store, + setStore, + assistantMessage(terminal.id, sessionID, { + activityID: INCIDENT_ACTIVITY, + revision: 10, + state: "progress", + }), + refetch, + ) + expect(firstAssistant(store, sessionID)?.activityProgress?.state).toBe("interrupted") + expect(firstAssistant(store, sessionID)?.activityProgress?.terminalReason).toBe("user_abort") + + // (c) An even older revision's progress event cannot displace the terminal revision either. + applyMessageUpdated( + store, + setStore, + assistantMessage(terminal.id, sessionID, { + activityID: INCIDENT_ACTIVITY, + revision: 8, + state: "progress", + }), + refetch, + ) + expect(firstAssistant(store, sessionID)?.activityProgress?.revision).toBe(10) + expect(firstAssistant(store, sessionID)?.activityProgress?.state).toBe("interrupted") + // None of these are integrity conflicts, so no forced reload is requested. + expect(refreshed).toEqual([]) + }) +}) + +describe("BUG-407-008 §7.2 App projection — trailing forced sync (scenarios 23-24)", () => { + test("23: conflict during an in-flight ordinary sync keeps canonical and runs one trailing forced sync after commit", async () => { + const sessionID = "ses_1" + const canonical = assistantMessage("msg_assistant", sessionID, { + activityID: INCIDENT_ACTIVITY, + revision: 9, + state: "progress", + }) + const [store, setStore] = createStore(baseState({ message: { [sessionID]: [canonical] } })) + + // Simulated durable read source: the authority commit flips this flag while the ordinary + // sync is still in flight; only the trailing forced sync may observe the committed state. + let authorityCommitted = false + const reads: Array<{ run: number; sawCommittedState: boolean }> = [] + let runs = 0 + let gate: (() => void) | undefined + const task = async () => { + runs += 1 + const run = runs + // The read happens at sync start — the ordinary sync reads BEFORE the authority commit, + // while the trailing forced sync reads after it. + reads.push({ run, sawCommittedState: authorityCommitted }) + if (run === 1) { + // The ordinary sync is still in flight when the conflict arrives. + await new Promise((resolve) => { + gate = resolve + }) + } + } + + const inflight = new Map>() + const trailing = new Set() + let forced: Promise | undefined + // Mirrors directory-sync wiring: refetchSession -> session.sync(sessionID, { force: true }), + // implemented via keyed runInflight so a forced call during an in-flight sync becomes a + // trailing forced sync instead of being swallowed. + const refetchSession = (id: string) => { + forced = runInflight(inflight, trailing, id, true, task) + } + + const ordinary = runInflight(inflight, trailing, sessionID, false, task) + await Promise.resolve() + expect(runs).toBe(1) + + // Authority commit happens, then the conflicting marker event arrives mid-flight. + applyMessageUpdated( + store, + setStore, + assistantMessage(canonical.id, sessionID, { + activityID: "activity-other", + revision: 0, + state: "final", + }), + refetchSession, + ) + + // Canonical marker is preserved against the conflicting incoming marker ... + expect(firstAssistant(store, sessionID)?.activityProgress).toEqual(canonical.activityProgress) + // ... and the conflict requested a refetch that was queued, not executed yet. + expect(forced).toBeDefined() + expect(runs).toBe(1) + expect(trailing.has(sessionID)).toBe(true) + + // The authority commit lands while the ordinary sync is still running. + authorityCommitted = true + gate?.() + await ordinary + await forced + + // Exactly one trailing forced sync ran, and its read happened AFTER the commit. + expect(runs).toBe(2) + expect(reads).toEqual([ + { run: 1, sawCommittedState: false }, + { run: 2, sawCommittedState: true }, + ]) + expect(trailing.size).toBe(0) + }) + + test("24: multiple conflicts in the same window coalesce into exactly one trailing forced sync, no loop", async () => { + const sessionID = "ses_1" + let runs = 0 + let gate: (() => void) | undefined + const task = async () => { + runs += 1 + if (runs === 1) { + await new Promise((resolve) => { + gate = resolve + }) + } + } + const inflight = new Map>() + const trailing = new Set() + + // Ordinary sync already in flight ... + const ordinary = runInflight(inflight, trailing, sessionID, false, task) + await Promise.resolve() + expect(runs).toBe(1) + + // ... then the SAME session receives several marker conflicts inside that window. Each one + // asks for a forced refetch (as applyDirectoryEvent would via refetchSession). + const forced = [ + runInflight(inflight, trailing, sessionID, true, task), + runInflight(inflight, trailing, sessionID, true, task), + runInflight(inflight, trailing, sessionID, true, task), + ] + await Promise.resolve() + // The in-flight ordinary Promise must not swallow them: a trailing run is armed. + expect(trailing.has(sessionID)).toBe(true) + expect(runs).toBe(1) + + gate?.() + await Promise.all([ordinary, ...forced]) + + // Exactly ONE trailing forced sync executed (ordinary + trailing = 2 runs total). + expect(runs).toBe(2) + expect(trailing.size).toBe(0) + + // No retry loop: after settling, nothing else runs. + for (let attempt = 0; attempt < 50; attempt += 1) await Promise.resolve() + expect(runs).toBe(2) + expect(trailing.size).toBe(0) + }) +}) + +describe("BUG-407-008 §7.2 App projection — pagination determinism (scenario 25)", () => { + test("25: newest-first paging, refresh and reconnect all converge to the same projection", async () => { + await createRoot(async (dispose) => { + const sessionID = "ses_0149b8afffffWlu80cVGdzFI9s" + const newest = assistantMessage("msg_incident_new", sessionID, { + activityID: INCIDENT_ACTIVITY, + revision: 9, + state: "progress", + }) + const older = assistantMessage("msg_incident_old", sessionID, { + activityID: INCIDENT_ACTIVITY, + revision: 8, + state: "progress", + }) + + const state = () => + createStore({ + path: { directory: "/repo" }, + session: [] as Array<{ id: string }>, + mcp: {}, + message: {} as Record, + part: {} as Record, + part_text_accum_delta: {} as Record, + todo: {} as Record, + session_diff: {} as Record, + session_status: {} as Record, + permission: {} as Record, + question: {} as Record, + }) + const current = state() + const serverSync = { + child() { + return current + }, + plan: { + async sync() {}, + }, + } as unknown as Parameters[1] + + let messageCalls = 0 + const sync = createDirSyncContext("/repo/main", serverSync, { + scope: ServerScope.local, + createClient() { + return { + session: { + async get() { + return { data: { id: sessionID } } + }, + // Durable authority: the newest page (revision 9) is served first with a cursor to + // the older page (revision 8); the older page has no further cursor. + async messages(input: { limit: number; before?: string }) { + messageCalls += 1 + if (input.before === "cursor-old") { + return { data: [{ info: older, parts: [] }], response: { headers: new Headers() } } + } + return { + data: [{ info: newest, parts: [] }], + response: { headers: new Headers({ "x-next-cursor": "cursor-old" }) }, + } + }, + }, + } + }, + } as unknown as Parameters[2]) + + const snapshot = () => + JSON.parse( + JSON.stringify( + (current[0].message[sessionID] ?? []).map((message) => ({ + id: message.id, + progress: message.role === "assistant" ? message.activityProgress ?? null : null, + })), + ), + ) + + current[1]("session", [current[0].session.length], { id: sessionID }) + + // Pass 1: load the newest page first, then page backwards to the older page. + await sync.session.sync(sessionID) + await sync.session.history.loadMore(sessionID) + const afterPaging = snapshot() + expect(afterPaging).toEqual([ + { id: newest.id, progress: newest.activityProgress }, + { id: older.id, progress: older.activityProgress }, + ]) + + // Pass 2: refresh/reconnect — evict all in-memory caches and rebuild from HTTP pages. + sync.session.evict(sessionID) + expect(current[0].message[sessionID]).toBeUndefined() + await sync.session.sync(sessionID, { force: true }) + await sync.session.history.loadMore(sessionID) + const afterReconnect = snapshot() + + // Identical projection regardless of page arrival order and cache state. + expect(afterReconnect).toEqual(afterPaging) + // Both pages were served twice (newest + older per pass); nothing else was fetched. + expect(messageCalls).toBe(4) + dispose() + }) + }) +}) diff --git a/packages/app/src/context/directory-sync.test.ts b/packages/app/src/context/directory-sync.test.ts index 0791d8ab..5f5baaf6 100644 --- a/packages/app/src/context/directory-sync.test.ts +++ b/packages/app/src/context/directory-sync.test.ts @@ -3,7 +3,7 @@ import { createRoot } from "solid-js" import { createStore } from "solid-js/store" import type { Message, Part } from "@deepagent-code/sdk/v2/client" import { ServerScope } from "@/utils/server-scope" -import { createDirSyncContext, runInflight } from "./directory-sync" +import { applyOptimisticAdd, createDirSyncContext, mergeOptimisticPage, runInflight } from "./directory-sync" const state = () => createStore({ @@ -412,6 +412,76 @@ describe("directory optimistic targeting", () => { if (result?.role === "assistant") expect(result.activityProgress).toEqual(canonical) }) + test("conflict recovery refetches only the authoritative tail, not the whole session", async () => { + let messageCalls = 0 + let sessionGetCalls = 0 + const messageRequests: Array<{ limit: number; before?: string }> = [] + let forcedStarted: (() => void) | undefined + const current = state() + const initial = { activityID: "activity-1", revision: 1, state: "progress" as const } + const canonical = { activityID: "activity-1", revision: 2, state: "final" as const } + const serverSync = { + child() { + return current + }, + plan: { + async sync() {}, + }, + } as unknown as Parameters[1] + const sync = createDirSyncContext("/repo/main", serverSync, { + scope: ServerScope.local, + createClient() { + return { + session: { + async get() { + sessionGetCalls += 1 + return { data: { id: "ses_1" } } + }, + async messages(input: { limit: number; before?: string }) { + messageCalls += 1 + messageRequests.push({ limit: input.limit, before: input.before }) + if (messageCalls === 3) forcedStarted?.() + const marker = + messageCalls === 2 + ? { activityID: "activity-conflict", revision: 0, state: "progress" as const } + : messageCalls === 3 + ? canonical + : initial + return { + data: [{ info: assistantMessage("msg_assistant", "ses_1", marker), parts: [] }], + response: { headers: new Headers(messageCalls === 1 ? { "x-next-cursor": "older" } : {}) }, + } + }, + }, + } + }, + } as unknown as Parameters[2]) + const sessionID = "ses_1" + current[1]("session", [current[0].session.length], { id: sessionID }) + await sync.session.sync(sessionID, { force: true }) + + const forced = new Promise((resolve) => { + forcedStarted = resolve + }) + await sync.session.history.loadMore(sessionID) + await forced + + expect(messageCalls).toBe(3) + // BUG-005 degradation: the recovery is a BOUNDED authoritative TAIL refetch (newest page, no + // `before`), NOT a full-session force reload — a full reload would re-fetch the session header. + expect(messageRequests[2]?.before).toBeUndefined() + expect(messageRequests[2]?.limit).toBeLessThanOrEqual(80) + expect(sessionGetCalls).toBe(1) + for (let attempt = 0; attempt < 10; attempt += 1) { + const message = current[0].message[sessionID]?.[0] + if (message?.role === "assistant" && message.activityProgress?.revision === canonical.revision) break + await Promise.resolve() + } + const result = current[0].message[sessionID]?.[0] + expect(result?.role).toBe("assistant") + if (result?.role === "assistant") expect(result.activityProgress).toEqual(canonical) + }) + test("does not let an older forced response overwrite a marker received during the request", async () => { let release: (() => void) | undefined const current = state() @@ -595,6 +665,105 @@ describe("directory optimistic targeting", () => { }) }) +describe("directory sync across the ID time wrap (BUG-407-012 root cause B)", () => { + // The 26th wrap of the 6-byte ID time field happened on 2026-08-14: the chronologically NEWER + // message carries a lexicographically SMALLER ID. These fixtures mirror the incident session. + const WRAP_OLD_ID = "msg_ffa88f0840015Xj7vIrcdNEJJB" // 2026-08-13 17:51:46 + const WRAP_NEW_ID = "msg_00d62a3c4001KqYw3o8wBBH6qm" // 2026-08-17 09:42:43 + const WRAP_OLD_TIME = 1786614706000 + const WRAP_NEW_TIME = 1786930963000 + const WRAP_OLD_PART = "prt_ffa88f084001aaaa" + const WRAP_NEW_PART = "prt_00d62a3c4001bbbb" + + const wrapUserMessage = (id: string, sessionID: string, created: number): Message => ({ + id, + sessionID, + role: "user", + time: { created }, + agent: "assistant", + model: { providerID: "openai", modelID: "gpt" }, + }) + + const wrapTextPart = (id: string, sessionID: string, messageID: string, start: number): Part => + ({ + id, + sessionID, + messageID, + type: "text", + text: id, + time: { start }, + }) as Part + + test("mergeOptimisticPage appends a wrapped newer optimistic message at the timeline tail", () => { + expect(WRAP_NEW_ID < WRAP_OLD_ID).toBe(true) // sanity: raw ID order is reversed + const older = wrapUserMessage(WRAP_OLD_ID, "ses_1", WRAP_OLD_TIME) + const newer = wrapUserMessage(WRAP_NEW_ID, "ses_1", WRAP_NEW_TIME) + + const merged = mergeOptimisticPage({ session: [older], part: [], complete: true }, [{ message: newer, parts: [] }]) + + expect(merged.session.map((message) => message.id)).toEqual([WRAP_OLD_ID, WRAP_NEW_ID]) + }) + + test("applyOptimisticAdd inserts a wrapped newer message after the older msg_ff... one", () => { + const older = wrapUserMessage(WRAP_OLD_ID, "ses_1", WRAP_OLD_TIME) + const newer = wrapUserMessage(WRAP_NEW_ID, "ses_1", WRAP_NEW_TIME) + const draft = { message: { ses_1: [older] }, part: {} } as { + message: Record + part: Record + } + + applyOptimisticAdd(draft, { sessionID: "ses_1", message: newer, parts: [] }) + + expect(draft.message.ses_1?.map((message) => message.id)).toEqual([WRAP_OLD_ID, WRAP_NEW_ID]) + }) + + test("a synced page keeps the server (time_created, id) order across the wrap", async () => { + const older = wrapUserMessage(WRAP_OLD_ID, "ses_1", WRAP_OLD_TIME) + const newer = wrapUserMessage(WRAP_NEW_ID, "ses_1", WRAP_NEW_TIME) + // Deliberately hand the page back-to-front plus a wrapped part pair in reversed order: the + // client-side re-sort must land on chronological order, not ID order. + const oldPart = wrapTextPart(WRAP_OLD_PART, "ses_1", WRAP_NEW_ID, WRAP_OLD_TIME) + const newPart = wrapTextPart(WRAP_NEW_PART, "ses_1", WRAP_NEW_ID, WRAP_NEW_TIME) + const current = state() + const serverSync = { + child() { + return current + }, + plan: { + async sync() {}, + }, + } as unknown as Parameters[1] + const sync = createDirSyncContext("/repo/main", serverSync, { + scope: ServerScope.local, + createClient() { + return { + session: { + async get() { + return { data: { id: "ses_1" } } + }, + async messages() { + return { + data: [ + { info: older, parts: [] }, + { info: newer, parts: [newPart, oldPart] }, + ], + response: { headers: new Headers() }, + } + }, + }, + } + }, + } as unknown as Parameters[2]) + const sessionID = "ses_1" + current[1]("session", [current[0].session.length], { id: sessionID }) + + await sync.session.sync(sessionID) + + expect(current[0].message[sessionID]?.map((message) => message.id)).toEqual([WRAP_OLD_ID, WRAP_NEW_ID]) + expect(current[0].part[WRAP_NEW_ID]?.map((part) => part.id)).toEqual([WRAP_OLD_PART, WRAP_NEW_PART]) + }) +}) + describe("directory forced session synchronization", () => { test("runs one trailing forced refresh after an ordinary refresh completes", async () => { const inflight = new Map>() diff --git a/packages/app/src/context/directory-sync.ts b/packages/app/src/context/directory-sync.ts index d6bc4a94..d223d515 100644 --- a/packages/app/src/context/directory-sync.ts +++ b/packages/app/src/context/directory-sync.ts @@ -12,6 +12,13 @@ import { import type { Message, Part } from "@deepagent-code/sdk/v2/client" import { SESSION_CACHE_LIMIT, dropSessionCaches, pickSessionCacheEvictions } from "./global-sync/session-cache" import { diffs as list, message as clean } from "@/utils/diffs" +import { + compareMessages, + compareParts, + findMessageIndex, + locateMessage, + locatePart, +} from "@/utils/message-order" import { createServerSdkContext, useServerSDK } from "./server-sdk" import { type createServerSyncContextInner } from "./server-sync" import { promptAdmissionClientMessageID } from "./global-sync/prompt-admission" @@ -20,8 +27,10 @@ import { mergeMessage } from "./global-sync/event-reducer" const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) type ActivityProgress = NonNullable["activityProgress"]> +// BUG-407-012 root cause B: ID order is not chronological order across the 6-byte ID time wrap; +// parts sort by (time.start, id), messages by (time.created, id). See utils/message-order.ts. function sortParts(parts: Part[]) { - return parts.filter((part) => !!part?.id).sort((a, b) => cmp(a.id, b.id)) + return parts.filter((part) => !!part?.id).sort((a, b) => compareParts(a, b)) } export async function runInflight( @@ -87,7 +96,7 @@ function mergePageMessages( conflict ||= result.conflict } return { - messages: [...messages.values()].sort((a, b) => cmp(a.id, b.id)), + messages: [...messages.values()].sort((a, b) => compareMessages(a, b)), conflict, } } @@ -142,7 +151,7 @@ type MessagePage = { const hasParts = (parts: Part[] | undefined, want: Part[]) => { if (!parts) return want.length === 0 - return want.every((part) => Binary.search(parts, part.id, (item) => item.id).found) + return want.every((part) => locatePart(parts, part).found) } const mergeParts = (parts: Part[] | undefined, want: Part[]) => { @@ -150,7 +159,7 @@ const mergeParts = (parts: Part[] | undefined, want: Part[]) => { const next = [...parts] let changed = false for (const part of want) { - const result = Binary.search(next, part.id, (item) => item.id) + const result = locatePart(next, part) if (result.found) continue next.splice(result.index, 0, part) changed = true @@ -180,7 +189,7 @@ export function mergeOptimisticPage(page: MessagePage, items: OptimisticItem[]) part.delete(item.message.id) continue } - const result = Binary.search(session, item.message.id, (message) => message.id) + const result = locateMessage(session, item.message) const found = result.found if (!found) session.splice(result.index, 0, item.message) @@ -209,7 +218,7 @@ export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddI return } if (messages) { - const result = Binary.search(messages, input.message.id, (m) => m.id) + const result = locateMessage(messages, input.message) if (result.found) messages[result.index] = input.message else messages.splice(result.index, 0, input.message) } else { @@ -221,8 +230,8 @@ export function applyOptimisticAdd(draft: OptimisticStore, input: OptimisticAddI export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticRemoveInput) { const messages = draft.message[input.sessionID] if (messages) { - const result = Binary.search(messages, input.messageID, (m) => m.id) - if (result.found) messages.splice(result.index, 1) + const index = findMessageIndex(messages, input.messageID) + if (index !== -1) messages.splice(index, 1) } delete draft.part[input.messageID] } @@ -230,7 +239,7 @@ export function applyOptimisticRemove(draft: OptimisticStore, input: OptimisticR function setOptimisticAdd(setStore: (...args: unknown[]) => void, input: OptimisticAddInput) { setStore("message", input.sessionID, (messages: Message[] | undefined) => { if (!messages) return [input.message] - const result = Binary.search(messages, input.message.id, (m) => m.id) + const result = locateMessage(messages, input.message) const next = [...messages] if (result.found) next[result.index] = input.message else next.splice(result.index, 0, input.message) @@ -242,10 +251,10 @@ function setOptimisticAdd(setStore: (...args: unknown[]) => void, input: Optimis function setOptimisticRemove(setStore: (...args: unknown[]) => void, input: OptimisticRemoveInput) { setStore("message", input.sessionID, (messages: Message[] | undefined) => { if (!messages) return messages - const result = Binary.search(messages, input.messageID, (m) => m.id) - if (!result.found) return messages + const index = findMessageIndex(messages, input.messageID) + if (index === -1) return messages const next = [...messages] - next.splice(result.index, 1) + next.splice(index, 1) return next }) setStore("part", input.messageID, undefined) @@ -397,7 +406,9 @@ export const createDirSyncContext = ( input.client.session.messages({ sessionID: input.sessionID, limit: input.limit, before: input.before }), ) const items = (messages.data ?? []).filter((x) => !!x?.info?.id) - const session = items.map((x) => clean(x.info)).sort((a, b) => cmp(a.id, b.id)) + // The server page is already in (time_created, id) order; re-sort with the same compound + // comparator so a wrapped `msg_00...` tail is never reordered ahead of `msg_ff...` history. + const session = items.map((x) => clean(x.info)).sort((a, b) => compareMessages(a, b)) const part = items.map((message) => ({ id: message.info.id, part: sortParts(message.parts) })) const cursor = messages.response?.headers.get("x-next-cursor") ?? undefined return { @@ -485,7 +496,24 @@ export const createDirSyncContext = ( draft.loading[key] = false }), ) - if (conflict && input.refetchOnConflict !== false) requestSessionSync?.(input.sessionID) + if (conflict && input.refetchOnConflict !== false) { + // BUG-005 residual: degrade conflict recovery from a FULL-session force reload + // (requestSessionSync) to an authoritative TAIL refetch. An activityProgress conflict + // only means the recent markers diverged; refetching the newest small page + // authoritatively reconciles them in place (mergePageMessages keys by id, so untouched + // messages are preserved) instead of refetching and re-rendering the whole session. + // force:true queues behind the in-flight page; refetchOnConflict:false bounds recursion. + void loadMessages({ + directory: input.directory, + client: input.client, + setStore: input.setStore, + sessionID: input.sessionID, + limit: Math.min(input.limit, initialMessagePageSize), + force: true, + authoritative: true, + refetchOnConflict: false, + }).catch(() => {}) + } }) }) } diff --git a/packages/app/src/context/global-sync/event-reducer.test.ts b/packages/app/src/context/global-sync/event-reducer.test.ts index 70397a69..e7bd1e47 100644 --- a/packages/app/src/context/global-sync/event-reducer.test.ts +++ b/packages/app/src/context/global-sync/event-reducer.test.ts @@ -546,6 +546,50 @@ describe("applyDirectoryEvent", () => { expect(refreshed).toEqual([sessionID]) }) + test("BUG-005: treats a same-activity revision mismatch as staleness, not a conflict", () => { + const sessionID = "ses_1" + const current = assistantMessage("msg_assistant", sessionID, { + activityID: "activity-1", + revision: 5, + state: "progress", + }) + const [store, setStore] = createStore(baseState({ message: { [sessionID]: [current] } })) + const refreshed: string[] = [] + const apply = (revision: number, state: "progress" | "final") => + applyDirectoryEvent({ + event: { + type: "message.updated", + properties: { + info: assistantMessage(current.id, sessionID, { + activityID: "activity-1", + revision, + state, + }), + }, + }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + refetchSession: (id) => refreshed.push(id), + }) + + // A stale page/event carrying an older revision must keep the higher one and never escalate. + apply(2, "progress") + expect(refreshed).toEqual([]) + expect( + (store.message[sessionID]?.[0] as Extract).activityProgress?.revision, + ).toBe(5) + + // A newer revision is adopted without triggering a canonical refresh either. + apply(7, "final") + expect(refreshed).toEqual([]) + const progress = (store.message[sessionID]?.[0] as Extract).activityProgress + expect(progress?.revision).toBe(7) + expect(progress?.state).toBe("final") + }) + test("allows a same-revision activity marker to advance but never regress", () => { const sessionID = "ses_1" const provisional = assistantMessage("msg_assistant", sessionID, { @@ -807,6 +851,83 @@ describe("applyDirectoryEvent", () => { }) }) +// BUG-407-012 root cause B: the 6-byte ID time field wrapped on 2026-08-14, so chronologically +// NEWER messages/parts carry lexicographically SMALLER IDs (`msg_00...` < `msg_ff...`). Realtime +// inserts must keep (time, id) chronological order instead of ID order. +describe("message ordering across the ID time wrap (BUG-407-012)", () => { + const WRAP_OLD_ID = "msg_ffa88f0840015Xj7vIrcdNEJJB" // 2026-08-13 17:51:46 + const WRAP_NEW_ID = "msg_00d62a3c4001KqYw3o8wBBH6qm" // 2026-08-17 09:42:43 + const WRAP_OLD_TIME = 1786614706000 + const WRAP_NEW_TIME = 1786930963000 + + const timedUserMessage = (id: string, sessionID: string, created: number) => + ({ ...userMessage(id, sessionID), time: { created } }) as Message + + const timedTextPart = (id: string, sessionID: string, messageID: string, start: number) => + ({ + id, + sessionID, + messageID, + type: "text", + text: id, + time: { start }, + }) as Part + + test("message.updated appends a chronologically newer msg_00... after the older msg_ff...", () => { + const sessionID = "ses_1" + expect(WRAP_NEW_ID < WRAP_OLD_ID).toBe(true) // sanity: raw ID order is reversed + const [store, setStore] = createStore( + baseState({ message: { [sessionID]: [timedUserMessage(WRAP_OLD_ID, sessionID, WRAP_OLD_TIME)] } }), + ) + + applyDirectoryEvent({ + event: { + type: "message.updated", + properties: { info: timedUserMessage(WRAP_NEW_ID, sessionID, WRAP_NEW_TIME) }, + }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + + expect(store.message[sessionID]?.map((message) => message.id)).toEqual([WRAP_OLD_ID, WRAP_NEW_ID]) + + // Identity-based removal still works in a (time, id)-ordered array. + applyDirectoryEvent({ + event: { type: "message.removed", properties: { sessionID, messageID: WRAP_OLD_ID } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + + expect(store.message[sessionID]?.map((message) => message.id)).toEqual([WRAP_NEW_ID]) + }) + + test("message.part.updated appends a chronologically newer wrapped part at the tail", () => { + const sessionID = "ses_1" + const messageID = WRAP_NEW_ID + const oldPart = timedTextPart("prt_ffa88f084001aaaa", sessionID, messageID, WRAP_OLD_TIME) + const newPart = timedTextPart("prt_00d62a3c4001bbbb", sessionID, messageID, WRAP_NEW_TIME) + expect(newPart.id < oldPart.id).toBe(true) // sanity: raw ID order is reversed + const [store, setStore] = createStore(baseState({ part: { [messageID]: [oldPart] } })) + + applyDirectoryEvent({ + event: { type: "message.part.updated", properties: { part: newPart } }, + store, + setStore, + push() {}, + directory: "/tmp", + loadLsp() {}, + }) + + expect(store.part[messageID]?.map((part) => part.id)).toEqual([oldPart.id, newPart.id]) + }) +}) + // Regression guard for the plan panel. The model pushes repeated plan.updated events that advance // step statuses; the reducer feeds them into a per-session plan store via reconcile. The previous // code used `{ key: "plan_id" }`, which reconcile applies recursively to the nested `steps[]` array diff --git a/packages/app/src/context/global-sync/event-reducer.ts b/packages/app/src/context/global-sync/event-reducer.ts index bb97d479..a0a3615f 100644 --- a/packages/app/src/context/global-sync/event-reducer.ts +++ b/packages/app/src/context/global-sync/event-reducer.ts @@ -14,6 +14,10 @@ import type { State, VcsCache, SessionPlan, SessionGoal, SessionPlanUpdateOption import { trimSessions } from "./session-trim" import { dropSessionCaches } from "./session-cache" import { diffs as list, message as clean } from "@/utils/diffs" +// BUG-407-012 root cause B: message/part arrays are kept in (time, id) chronological order, not +// ID lexicographic order — the 6-byte ID time field wraps every 2^36 ms, so ID order alone is +// not time order. Bare-ID events (remove/delta) must locate by identity scan, not binary search. +import { findMessageIndex, findPartIndex, locateMessage, locatePart } from "@/utils/message-order" import { promptAdmissionClientMessageID } from "./prompt-admission" const SKIP_PARTS = new Set(["patch", "step-start", "step-finish"]) @@ -47,8 +51,15 @@ export function mergeMessage(current: Message, incoming: Message) { } if (!existing) return { message: incoming, conflict: false } if (!next) return { message: { ...incoming, activityProgress: existing }, conflict: false } - if (existing.activityID !== next.activityID || existing.revision !== next.revision) + if (existing.activityID !== next.activityID) return { message: { ...incoming, activityProgress: existing }, conflict: true } + // BUG-005: revisions are monotonic within one activity (server commits revision+1). The paged + // snapshot routinely lags behind the realtime stream, so a revision mismatch is staleness, not a + // conflict — take the higher revision instead of escalating to a whole-session force reload. + if (existing.revision !== next.revision) + return existing.revision > next.revision + ? { message: { ...incoming, activityProgress: existing }, conflict: false } + : { message: incoming, conflict: false } if (existing.state === next.state) return { message: incoming, conflict: false } const rank = (progress: ActivityProgress) => progress.state === "provisional" ? 0 : progress.state === "progress" ? 1 : 2 @@ -274,8 +285,8 @@ export function applyDirectoryEvent(input: { produce((draft) => { const messages = draft.message[info.sessionID] if (messages) { - const result = Binary.search(messages, clientMessageID, (message) => message.id) - if (result.found) messages.splice(result.index, 1) + const index = findMessageIndex(messages, clientMessageID) + if (index !== -1) messages.splice(index, 1) } const parts = draft.part[clientMessageID] if (parts) { @@ -290,7 +301,7 @@ export function applyDirectoryEvent(input: { input.setStore("message", info.sessionID, [info]) break } - const result = Binary.search(messages, info.id, (m) => m.id) + const result = locateMessage(messages, info) if (result.found) { const existing = messages[result.index]! const merged = mergeMessage(existing, info) @@ -321,8 +332,8 @@ export function applyDirectoryEvent(input: { produce((draft) => { const messages = draft.message[props.sessionID] if (messages) { - const result = Binary.search(messages, props.messageID, (m) => m.id) - if (result.found) messages.splice(result.index, 1) + const index = findMessageIndex(messages, props.messageID) + if (index !== -1) messages.splice(index, 1) } const parts = draft.part[props.messageID] if (parts) { @@ -348,7 +359,7 @@ export function applyDirectoryEvent(input: { input.setStore("part", part.messageID, [part]) break } - const result = Binary.search(parts, part.id, (p) => p.id) + const result = locatePart(parts, part) if (result.found) { input.setStore("part", part.messageID, result.index, reconcile(part)) break @@ -371,15 +382,15 @@ export function applyDirectoryEvent(input: { ) const parts = input.store.part[props.messageID] if (!parts) break - const result = Binary.search(parts, props.partID, (p) => p.id) - if (result.found) { + const index = findPartIndex(parts, props.partID) + if (index !== -1) { input.setStore( produce((draft) => { const list = draft.part[props.messageID] if (!list) return - const next = Binary.search(list, props.partID, (p) => p.id) - if (!next.found) return - list.splice(next.index, 1) + const next = findPartIndex(list, props.partID) + if (next === -1) return + list.splice(next, 1) if (list.length === 0) delete draft.part[props.messageID] }), ) @@ -390,14 +401,14 @@ export function applyDirectoryEvent(input: { const props = event.properties as { messageID: string; partID: string; field: string; delta: string } const parts = input.store.part[props.messageID] if (!parts) break - const result = Binary.search(parts, props.partID, (p) => p.id) - if (!result.found) break + const index = findPartIndex(parts, props.partID) + if (index === -1) break input.setStore("part_text_accum_delta", props.partID, (existing) => (existing ?? "") + props.delta) input.setStore( "part", props.messageID, produce((draft) => { - const part = draft[result.index] + const part = draft[index] const field = props.field as keyof typeof part const existing = part[field] as string | undefined ;(part[field] as string) = (existing ?? "") + props.delta diff --git a/packages/app/src/context/layout.tsx b/packages/app/src/context/layout.tsx index 231fa23c..ff4e37c1 100644 --- a/packages/app/src/context/layout.tsx +++ b/packages/app/src/context/layout.tsx @@ -120,6 +120,8 @@ type SessionView = { | "profile" | "debug" | "im" + // PARITY-001: cross-session cost/token overview panel. + | "stats" | "oversight" // Movable panel views can also live in the side panel. | "terminal" diff --git a/packages/app/src/context/models.tsx b/packages/app/src/context/models.tsx index 030f0945..0607e900 100644 --- a/packages/app/src/context/models.tsx +++ b/packages/app/src/context/models.tsx @@ -1,9 +1,12 @@ -import { createMemo } from "solid-js" +import { createMemo, onMount } from "solid-js" import { createStore } from "solid-js/store" import { DateTime } from "luxon" import { filter, firstBy, flat, groupBy, mapValues, pipe, uniqueBy, values } from "remeda" import { createSimpleContext } from "@deepagent-code/ui/context" import { useProviders } from "@/hooks/use-providers" +import { useServerSync } from "@/context/server-sync" +import { useLanguage } from "@/context/language" +import { showToast } from "@/utils/toast" import { Persist, persisted } from "@/utils/persist" export type ModelKey = { providerID: string; modelID: string } @@ -27,6 +30,8 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( gate: false, init: () => { const providers = useProviders() + const serverSync = useServerSync() + const language = useLanguage() const [store, setStore, _, ready] = persisted( Persist.global("model", ["model.v1"]), @@ -87,9 +92,45 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( const latestSet = createMemo(() => new Set(latest().map((x) => modelKey(x)))) + // PARITY-004 / GUI 批 — 模型启停服务端真相源:可见性(启/停)的权威来源是服务端 config + // `provider[pID].models[mID].disabled`(plugin/provider.ts 已映射 model.enabled=!disabled)。 + // localStorage(store.user)仅作迁移来源与乐观缓存,不再是真相。 + const serverDisabled = (model: ModelKey): boolean | undefined => { + const provider = serverSync.data.config.provider?.[model.providerID] + const disabled = provider?.models?.[model.modelID]?.disabled + return typeof disabled === "boolean" ? disabled : undefined + } + + const setServerDisabled = async (model: ModelKey, disabled: boolean) => { + const config = serverSync.data.config + const provider = config.provider?.[model.providerID] + const current = provider?.models?.[model.modelID] + await serverSync.updateConfig({ + ...config, + provider: { + ...config.provider, + [model.providerID]: { + ...provider, + models: { + ...provider?.models, + [model.modelID]: { ...current, disabled }, + }, + }, + }, + }) + } + const visibility = createMemo(() => { const map = new Map() + // localStorage 先铺底(迁移前的旧值) for (const item of store.user) map.set(`${item.providerID}:${item.modelID}`, item.visibility) + // 服务端真相覆盖(权威) + for (const model of available()) { + const key = `${model.provider.id}:${model.id}` + const disabled = serverDisabled({ providerID: model.provider.id, modelID: model.id }) + if (disabled === true) map.set(key, "hide") + else if (disabled === false) map.set(key, "show") + } return map }) @@ -115,14 +156,36 @@ export const { use: useModels, provider: ModelsProvider } = createSimpleContext( const find = (key: ModelKey) => list().find((m) => m.id === key.modelID && m.provider.id === key.providerID) function update(model: ModelKey, state: Visibility) { + // 乐观写 localStorage(即时 UI),真相异步写服务端 config。 + const key = modelKey(model) const index = store.user.findIndex((x) => x.modelID === model.modelID && x.providerID === model.providerID) if (index >= 0) { setStore("user", index, (current) => ({ ...current, visibility: state })) - return + } else { + setStore("user", store.user.length, { ...model, visibility: state }) } - setStore("user", store.user.length, { ...model, visibility: state }) + void setServerDisabled(model, state === "hide").catch(() => { + // 服务端写失败:回滚乐观态——移除该 model 的本地覆盖,visibility 回落到服务端真相/默认。 + setStore("user", (items) => items.filter((item) => modelKey(item) !== key)) + showToast({ title: language.t("common.requestFailed") }) + }) } + // 一次性迁移:把 localStorage 里的显式 show/hide 上收服务端,随后服务端即真相。 + onMount(() => { + const pending = store.user.slice() + if (pending.length === 0) return + void (async () => { + for (const item of pending) { + if (serverDisabled({ providerID: item.providerID, modelID: item.modelID }) !== undefined) continue + await setServerDisabled({ providerID: item.providerID, modelID: item.modelID }, item.visibility === "hide").catch( + () => undefined, + ) + } + setStore("user", []) + })() + }) + const visible = (model: ModelKey) => { const key = modelKey(model) const state = visibility().get(key) diff --git a/packages/app/src/context/permission-auto-respond.ts b/packages/app/src/context/permission-auto-respond.ts index 38670af2..3347f5f0 100644 --- a/packages/app/src/context/permission-auto-respond.ts +++ b/packages/app/src/context/permission-auto-respond.ts @@ -1,5 +1,10 @@ import { base64Encode } from "@deepagent-code/core/util/encode" +// Mutating-tool set is shared with the CLI read-only mode (PARITY-002): the +// GUI directory read-only guard and `run --permission-mode read-only` must +// agree on which permissions are denied, so the constant lives in core. +export { MUTATING_PERMISSIONS, isMutatingPermission } from "@deepagent-code/core/permission/mutating" + export function acceptKey(sessionID: string, directory?: string) { if (!directory) return sessionID return `${base64Encode(directory)}/${sessionID}` @@ -16,15 +21,6 @@ export function isDirectoryReadOnly(readOnly: Record, directory return readOnly[key] ?? false } -// Tools that write, execute, or otherwise mutate state — denied in read-only mode. -// Everything else (read/glob/grep/list/lsp/webfetch/websearch/todowrite/…) is left to the -// normal permission flow so the agent can still inspect the workspace. -const MUTATING_PERMISSIONS = new Set(["edit", "write", "patch", "bash", "task", "external_directory"]) - -export function isMutatingPermission(permission: string) { - return MUTATING_PERMISSIONS.has(permission) -} - function accepted(autoAccept: Record, sessionID: string, directory?: string) { const key = acceptKey(sessionID, directory) const directoryKey = directory ? directoryAcceptKey(directory) : undefined diff --git a/packages/app/src/i18n/ar.ts b/packages/app/src/i18n/ar.ts index f94a0fe2..ca692879 100644 --- a/packages/app/src/i18n/ar.ts +++ b/packages/app/src/i18n/ar.ts @@ -499,6 +499,8 @@ export const dict = { "session.share.popover.description.shared": "هذه الجلسة عامة على الويب. يمكن لأي شخص لديه الرابط الوصول إليها.", "session.share.popover.description.unshared": "شارك الجلسة علنًا على الويب. ستكون متاحة لأي شخص لديه الرابط.", "session.share.action.share": "مشاركة", + "session.export.action": "تصدير اللقطة", + "session.export.success": "تم تصدير لقطة الجلسة", "session.share.action.publish": "نشر", "session.share.action.publishing": "جارٍ النشر...", "session.share.action.unpublish": "إلغاء النشر", @@ -518,6 +520,7 @@ export const dict = { "common.closeTab": "إغلاق علامة التبويب", "common.dismiss": "رفض", "common.requestFailed": "فشل الطلب", + "common.active": "نشط", "common.moreOptions": "مزيد من الخيارات", "common.learnMore": "اعرف المزيد", "common.rename": "إعادة تسمية", @@ -682,6 +685,14 @@ export const dict = { "settings.providers.section.connected": "الموفرون المتصلون", "settings.providers.connected.empty": "لا يوجد موفرون متصلون", "settings.providers.section.popular": "الموفرون الشائعون", + "settings.providers.section.consoleAccount": "حساب Console", + "settings.providers.console.activeOrg": "المنظمة النشطة", + "settings.providers.console.noOrg": "لم يتم اختيار منظمة", + "settings.providers.console.switchOrg": "تبديل المنظمة", + "settings.providers.console.empty": "لا توجد مؤسسات Console قابلة للتبديل", + "settings.providers.console.managedCount": "{{count}} من مزودي الخدمة المدارة عبر Console", + "settings.providers.console.switch.toast.title": "تم تبديل منظمة Console", + "settings.providers.console.switch.toast.description": "الآن يتم استخدام {{org}}", "settings.providers.tag.environment": "البيئة", "settings.providers.tag.config": "التكوين", "settings.providers.tag.custom": "مخصص", @@ -857,6 +868,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (لقطة)", + "settings.import.snapshot.file": "حزمة اللقطة", + "settings.import.snapshot.file.hint": "اختر حزمة لقطة الجلسة المصدّرة (.json). سيتم استيرادها كجلسة جديدة قابلة للمتابعة.", + "settings.import.snapshot.noFile": "اختر ملف حزمة اللقطة أولاً", + "settings.import.snapshot.done": "تم استيراد اللقطة كجلسة جديدة قابلة للمتابعة", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/br.ts b/packages/app/src/i18n/br.ts index 31699b30..115fbede 100644 --- a/packages/app/src/i18n/br.ts +++ b/packages/app/src/i18n/br.ts @@ -505,6 +505,8 @@ export const dict = { "session.share.popover.description.unshared": "Compartilhar sessão publicamente na web. Estará acessível para qualquer pessoa com o link.", "session.share.action.share": "Compartilhar", + "session.export.action": "Exportar snapshot", + "session.export.success": "Snapshot de sessao exportado", "session.share.action.publish": "Publicar", "session.share.action.publishing": "Publicando...", "session.share.action.unpublish": "Cancelar publicação", @@ -525,6 +527,7 @@ export const dict = { "common.closeTab": "Fechar aba", "common.dismiss": "Descartar", "common.requestFailed": "Requisição falhou", + "common.active": "Ativa", "common.moreOptions": "Mais opções", "common.learnMore": "Saiba mais", "common.rename": "Renomear", @@ -694,6 +697,14 @@ export const dict = { "settings.providers.section.connected": "Provedores conectados", "settings.providers.connected.empty": "Nenhum provedor conectado", "settings.providers.section.popular": "Provedores populares", + "settings.providers.section.consoleAccount": "Conta Console", + "settings.providers.console.activeOrg": "Organização ativa", + "settings.providers.console.noOrg": "Nenhuma organização selecionada", + "settings.providers.console.switchOrg": "Trocar organização", + "settings.providers.console.empty": "Nenhuma organização Console disponível para troca", + "settings.providers.console.managedCount": "{{count}} provedores gerenciados pelo Console", + "settings.providers.console.switch.toast.title": "Organização Console trocada", + "settings.providers.console.switch.toast.description": "Agora usando {{org}}", "settings.providers.tag.environment": "Ambiente", "settings.providers.tag.config": "Configuração", "settings.providers.tag.custom": "Personalizado", @@ -871,6 +882,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (snapshot)", + "settings.import.snapshot.file": "Pacote de snapshot", + "settings.import.snapshot.file.hint": "Escolha um pacote de snapshot de sessao exportado (.json). Ele sera importado como uma nova sessao continuavel.", + "settings.import.snapshot.noFile": "Escolha primeiro um arquivo de pacote de snapshot", + "settings.import.snapshot.done": "Snapshot importado como uma nova sessao continuavel", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/bs.ts b/packages/app/src/i18n/bs.ts index d97be89f..a8285882 100644 --- a/packages/app/src/i18n/bs.ts +++ b/packages/app/src/i18n/bs.ts @@ -563,6 +563,8 @@ export const dict = { "session.share.popover.description.shared": "Ova sesija je javna na webu. Dostupna je svima koji imaju link.", "session.share.popover.description.unshared": "Podijeli sesiju javno na webu. Biće dostupna svima koji imaju link.", "session.share.action.share": "Podijeli", + "session.export.action": "Izvezi snimak", + "session.export.success": "Snimak sesije izvezen", "session.share.action.publish": "Objavi", "session.share.action.publishing": "Objavljivanje...", "session.share.action.unpublish": "Poništi objavu", @@ -586,6 +588,7 @@ export const dict = { "common.closeTab": "Zatvori karticu", "common.dismiss": "Odbaci", "common.requestFailed": "Zahtjev nije uspio", + "common.active": "Aktivna", "common.moreOptions": "Više opcija", "common.learnMore": "Saznaj više", "common.rename": "Preimenuj", @@ -766,6 +769,14 @@ export const dict = { "settings.providers.section.connected": "Povezani provajderi", "settings.providers.connected.empty": "Nema povezanih provajdera", "settings.providers.section.popular": "Popularni provajderi", + "settings.providers.section.consoleAccount": "Console račun", + "settings.providers.console.activeOrg": "Aktivna organizacija", + "settings.providers.console.noOrg": "Nije odabrana organizacija", + "settings.providers.console.switchOrg": "Promijeni organizaciju", + "settings.providers.console.empty": "Nema Console organizacija za promjenu", + "settings.providers.console.managedCount": "{{count}} provajdera kojima upravlja Console", + "settings.providers.console.switch.toast.title": "Console organizacija promijenjena", + "settings.providers.console.switch.toast.description": "Sada se koristi {{org}}", "settings.providers.tag.environment": "Okruženje", "settings.providers.tag.config": "Konfiguracija", "settings.providers.tag.custom": "Prilagođeno", @@ -948,6 +959,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (snimak)", + "settings.import.snapshot.file": "Paket snimka", + "settings.import.snapshot.file.hint": "Odaberite izvezeni paket snimka sesije (.json). Bice uvezen kao nova nastavljiva sesija.", + "settings.import.snapshot.noFile": "Prvo odaberite datoteku paketa snimka", + "settings.import.snapshot.done": "Snimak uvezen kao nova nastavljiva sesija", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/da.ts b/packages/app/src/i18n/da.ts index b725d224..66a68347 100644 --- a/packages/app/src/i18n/da.ts +++ b/packages/app/src/i18n/da.ts @@ -560,6 +560,8 @@ export const dict = { "session.share.popover.description.unshared": "Del session offentligt på nettet. Den vil være tilgængelig for alle med linket.", "session.share.action.share": "Del", + "session.export.action": "Eksporter snapshot", + "session.export.success": "Sessions-snapshot eksporteret", "session.share.action.publish": "Udgiv", "session.share.action.publishing": "Udgiver...", "session.share.action.unpublish": "Afpublicer", @@ -582,6 +584,7 @@ export const dict = { "common.closeTab": "Luk fane", "common.dismiss": "Afvis", "common.requestFailed": "Forespørgsel mislykkedes", + "common.active": "Aktiv", "common.moreOptions": "Flere muligheder", "common.learnMore": "Lær mere", "common.rename": "Omdøb", @@ -759,6 +762,14 @@ export const dict = { "settings.providers.section.connected": "Forbundne udbydere", "settings.providers.connected.empty": "Ingen forbundne udbydere", "settings.providers.section.popular": "Populære udbydere", + "settings.providers.section.consoleAccount": "Console-konto", + "settings.providers.console.activeOrg": "Aktiv organisation", + "settings.providers.console.noOrg": "Ingen organisation valgt", + "settings.providers.console.switchOrg": "Skift organisation", + "settings.providers.console.empty": "Ingen Console-organisationer at skifte mellem", + "settings.providers.console.managedCount": "{{count}} Console-administrerede udbydere", + "settings.providers.console.switch.toast.title": "Console-organisation skiftet", + "settings.providers.console.switch.toast.description": "Bruger nu {{org}}", "settings.providers.tag.environment": "Miljø", "settings.providers.tag.config": "Konfiguration", "settings.providers.tag.custom": "Brugerdefineret", @@ -941,6 +952,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (snapshot)", + "settings.import.snapshot.file": "Snapshot-bundle", + "settings.import.snapshot.file.hint": "Vaelg en eksporteret sessions-snapshot-bundle (.json). Den importeres som en ny fortsaetbar session.", + "settings.import.snapshot.noFile": "Vaelg foerst en snapshot-bundle-fil", + "settings.import.snapshot.done": "Snapshot importeret som en ny fortsaetbar session", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/de.ts b/packages/app/src/i18n/de.ts index f1972566..4c141d1b 100644 --- a/packages/app/src/i18n/de.ts +++ b/packages/app/src/i18n/de.ts @@ -513,6 +513,8 @@ export const dict = { "session.share.popover.description.unshared": "Sitzung öffentlich im Web teilen. Sie wird für jeden mit dem Link zugänglich sein.", "session.share.action.share": "Teilen", + "session.export.action": "Snapshot exportieren", + "session.export.success": "Session-Snapshot exportiert", "session.share.action.publish": "Veröffentlichen", "session.share.action.publishing": "Veröffentliche...", "session.share.action.unpublish": "Veröffentlichung aufheben", @@ -533,6 +535,7 @@ export const dict = { "common.closeTab": "Tab schließen", "common.dismiss": "Verwerfen", "common.requestFailed": "Anfrage fehlgeschlagen", + "common.active": "Aktiv", "common.moreOptions": "Weitere Optionen", "common.learnMore": "Mehr erfahren", "common.rename": "Umbenennen", @@ -704,6 +707,14 @@ export const dict = { "settings.providers.section.connected": "Verbundene Anbieter", "settings.providers.connected.empty": "Keine verbundenen Anbieter", "settings.providers.section.popular": "Beliebte Anbieter", + "settings.providers.section.consoleAccount": "Console-Konto", + "settings.providers.console.activeOrg": "Aktive Organisation", + "settings.providers.console.noOrg": "Keine Organisation ausgewählt", + "settings.providers.console.switchOrg": "Organisation wechseln", + "settings.providers.console.empty": "Keine wechselbaren Console-Organisationen", + "settings.providers.console.managedCount": "{{count}} Console-verwaltete Anbieter", + "settings.providers.console.switch.toast.title": "Console-Organisation gewechselt", + "settings.providers.console.switch.toast.description": "Jetzt wird {{org}} verwendet", "settings.providers.tag.environment": "Umgebung", "settings.providers.tag.config": "Konfiguration", "settings.providers.tag.custom": "Benutzerdefiniert", @@ -887,6 +898,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (Snapshot)", + "settings.import.snapshot.file": "Snapshot-Bundle", + "settings.import.snapshot.file.hint": "Waehlen Sie ein exportiertes Session-Snapshot-Bundle (.json). Es wird als neue fortsetzbare Sitzung importiert.", + "settings.import.snapshot.noFile": "Waehlen Sie zuerst eine Snapshot-Bundle-Datei", + "settings.import.snapshot.done": "Snapshot als neue fortsetzbare Sitzung importiert", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts index 1882e11d..368fe42d 100644 --- a/packages/app/src/i18n/en.ts +++ b/packages/app/src/i18n/en.ts @@ -1023,6 +1023,8 @@ export const dict = { "session.share.popover.description.unshared": "Share session publicly on the web. It will be accessible to anyone with the link.", "session.share.action.share": "Share", + "session.export.action": "Export snapshot", + "session.export.success": "Session snapshot exported", "session.share.action.publish": "Publish", "session.share.action.publishing": "Publishing...", "session.share.action.unpublish": "Unpublish", @@ -1060,6 +1062,7 @@ export const dict = { "common.dismiss": "Dismiss", "common.moreCountSuffix": " (+{{count}} more)", "common.requestFailed": "Request failed", + "common.active": "Active", "common.moreOptions": "More options", "common.learnMore": "Learn more", "common.rename": "Rename", @@ -1174,6 +1177,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (snapshot)", + "settings.import.snapshot.file": "Snapshot bundle", + "settings.import.snapshot.file.hint": "Choose an exported session snapshot bundle (.json). It imports as a new continuable session.", + "settings.import.snapshot.noFile": "Choose a snapshot bundle file first", + "settings.import.snapshot.done": "Snapshot imported as a new continuable session", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", @@ -1483,6 +1491,14 @@ export const dict = { "settings.providers.connected.empty": "No connected providers", "settings.providers.connected.environmentDescription": "Connected from your environment variables", "settings.providers.section.popular": "Popular providers", + "settings.providers.section.consoleAccount": "Console account", + "settings.providers.console.activeOrg": "Active organization", + "settings.providers.console.noOrg": "No organization selected", + "settings.providers.console.switchOrg": "Switch organization", + "settings.providers.console.empty": "No switchable Console organizations", + "settings.providers.console.managedCount": "{{count}} Console-managed providers", + "settings.providers.console.switch.toast.title": "Switched Console organization", + "settings.providers.console.switch.toast.description": "Now using {{org}}", "settings.providers.custom.description": "Add an OpenAI-compatible provider by base URL.", "settings.providers.tag.environment": "Environment", "settings.providers.tag.config": "Config", @@ -1601,6 +1617,22 @@ export const dict = { "session.context.copyRef": "Copy stable reference", "session.context.provenance": "Provenance", "session.context.relations": "Relations", + "session.stats.title": "Usage & cost", + "session.stats.overview": "Overview", + "session.stats.sessions": "Sessions", + "session.stats.days": "Days", + "session.stats.costTokens": "Cost & tokens", + "session.stats.totalCost": "Total cost", + "session.stats.costPerDay": "Avg cost/day", + "session.stats.tokensPerSession": "Avg tokens/session", + "session.stats.medianTokens": "Median tokens/session", + "session.stats.input": "Input", + "session.stats.output": "Output", + "session.stats.reasoning": "Reasoning", + "session.stats.cacheRead": "Cache read", + "session.stats.cacheWrite": "Cache write", + "session.stats.modelUsage": "Model usage", + "session.stats.noData": "No session usage data yet.", "session.context.summary.complete": "Complete", "session.context.summary.partial": "Partial", "session.context.summary.empty": "Empty", diff --git a/packages/app/src/i18n/es.ts b/packages/app/src/i18n/es.ts index 0d6b9085..bdbbf99d 100644 --- a/packages/app/src/i18n/es.ts +++ b/packages/app/src/i18n/es.ts @@ -566,6 +566,8 @@ export const dict = { "session.share.popover.description.unshared": "Compartir sesión públicamente en la web. Será accesible para cualquiera con el enlace.", "session.share.action.share": "Compartir", + "session.export.action": "Exportar instantanea", + "session.export.success": "Instantanea de sesion exportada", "session.share.action.publish": "Publicar", "session.share.action.publishing": "Publicando...", "session.share.action.unpublish": "Despublicar", @@ -589,6 +591,7 @@ export const dict = { "common.closeTab": "Cerrar pestaña", "common.dismiss": "Descartar", "common.requestFailed": "Solicitud fallida", + "common.active": "Activa", "common.moreOptions": "Más opciones", "common.learnMore": "Saber más", "common.rename": "Renombrar", @@ -772,6 +775,14 @@ export const dict = { "settings.providers.section.connected": "Proveedores conectados", "settings.providers.connected.empty": "No hay proveedores conectados", "settings.providers.section.popular": "Proveedores populares", + "settings.providers.section.consoleAccount": "Cuenta de Console", + "settings.providers.console.activeOrg": "Organización activa", + "settings.providers.console.noOrg": "Ninguna organización seleccionada", + "settings.providers.console.switchOrg": "Cambiar organización", + "settings.providers.console.empty": "No hay organizaciones de Console intercambiables", + "settings.providers.console.managedCount": "{{count}} proveedores gestionados por Console", + "settings.providers.console.switch.toast.title": "Organización de Console cambiada", + "settings.providers.console.switch.toast.description": "Ahora usando {{org}}", "settings.providers.tag.environment": "Entorno", "settings.providers.tag.config": "Configuración", "settings.providers.tag.custom": "Personalizado", @@ -954,6 +965,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (instantanea)", + "settings.import.snapshot.file": "Paquete de instantanea", + "settings.import.snapshot.file.hint": "Elija un paquete de instantanea de sesion exportado (.json). Se importara como una nueva sesion continuable.", + "settings.import.snapshot.noFile": "Elija primero un archivo de paquete de instantanea", + "settings.import.snapshot.done": "Instantanea importada como una nueva sesion continuable", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/fr.ts b/packages/app/src/i18n/fr.ts index 93df50ca..275ec09e 100644 --- a/packages/app/src/i18n/fr.ts +++ b/packages/app/src/i18n/fr.ts @@ -510,6 +510,8 @@ export const dict = { "session.share.popover.description.unshared": "Partager la session publiquement sur le web. Elle sera accessible à toute personne disposant du lien.", "session.share.action.share": "Partager", + "session.export.action": "Exporter le snapshot", + "session.export.success": "Snapshot de session exporte", "session.share.action.publish": "Publier", "session.share.action.publishing": "Publication...", "session.share.action.unpublish": "Dépublier", @@ -530,6 +532,7 @@ export const dict = { "common.closeTab": "Fermer l'onglet", "common.dismiss": "Ignorer", "common.requestFailed": "La demande a échoué", + "common.active": "Active", "common.moreOptions": "Plus d'options", "common.learnMore": "En savoir plus", "common.rename": "Renommer", @@ -701,6 +704,14 @@ export const dict = { "settings.providers.section.connected": "Fournisseurs connectés", "settings.providers.connected.empty": "Aucun fournisseur connecté", "settings.providers.section.popular": "Fournisseurs populaires", + "settings.providers.section.consoleAccount": "Compte Console", + "settings.providers.console.activeOrg": "Organisation active", + "settings.providers.console.noOrg": "Aucune organisation sélectionnée", + "settings.providers.console.switchOrg": "Changer d'organisation", + "settings.providers.console.empty": "Aucune organisation Console commutable", + "settings.providers.console.managedCount": "{{count}} fournisseurs gérés par Console", + "settings.providers.console.switch.toast.title": "Organisation Console changée", + "settings.providers.console.switch.toast.description": "Utilisation de {{org}}", "settings.providers.tag.environment": "Environnement", "settings.providers.tag.config": "Configuration", "settings.providers.tag.custom": "Personnalisé", @@ -881,6 +892,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (snapshot)", + "settings.import.snapshot.file": "Bundle de snapshot", + "settings.import.snapshot.file.hint": "Choisissez un bundle de snapshot de session exporte (.json). Il sera importe comme une nouvelle session continuable.", + "settings.import.snapshot.noFile": "Choisissez d'abord un fichier de bundle de snapshot", + "settings.import.snapshot.done": "Snapshot importe comme une nouvelle session continuable", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/ja.ts b/packages/app/src/i18n/ja.ts index c79ca0f3..ce79c81d 100644 --- a/packages/app/src/i18n/ja.ts +++ b/packages/app/src/i18n/ja.ts @@ -504,6 +504,8 @@ export const dict = { "session.share.popover.description.unshared": "セッションをウェブで公開します。リンクを知っている人なら誰でもアクセスできるようになります。", "session.share.action.share": "共有", + "session.export.action": "スナップショットをエクスポート", + "session.export.success": "セッションスナップショットをエクスポートしました", "session.share.action.publish": "公開", "session.share.action.publishing": "公開中...", "session.share.action.unpublish": "非公開にする", @@ -524,6 +526,7 @@ export const dict = { "common.closeTab": "タブを閉じる", "common.dismiss": "閉じる", "common.requestFailed": "リクエスト失敗", + "common.active": "アクティブ", "common.moreOptions": "その他のオプション", "common.learnMore": "詳細", "common.rename": "名前変更", @@ -692,6 +695,14 @@ export const dict = { "settings.providers.section.connected": "接続済みプロバイダー", "settings.providers.connected.empty": "接続済みプロバイダーはありません", "settings.providers.section.popular": "人気のプロバイダー", + "settings.providers.section.consoleAccount": "Console アカウント", + "settings.providers.console.activeOrg": "アクティブな組織", + "settings.providers.console.noOrg": "組織が選択されていません", + "settings.providers.console.switchOrg": "組織を切り替え", + "settings.providers.console.empty": "切り替え可能な Console 組織がありません", + "settings.providers.console.managedCount": "Console 管理のプロバイダー {{count}} 件", + "settings.providers.console.switch.toast.title": "Console 組織を切り替えました", + "settings.providers.console.switch.toast.description": "現在 {{org}} を使用中", "settings.providers.tag.environment": "環境", "settings.providers.tag.config": "設定", "settings.providers.tag.custom": "カスタム", @@ -867,6 +878,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code(スナップショット)", + "settings.import.snapshot.file": "スナップショットバンドル", + "settings.import.snapshot.file.hint": "エクスポートされたセッションスナップショットバンドル(.json)を選択してください。継続可能な新しいセッションとしてインポートされます。", + "settings.import.snapshot.noFile": "先にスナップショットバンドルファイルを選択してください", + "settings.import.snapshot.done": "スナップショットは継続可能な新しいセッションとしてインポートされました", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/ko.ts b/packages/app/src/i18n/ko.ts index 1b742aee..8d2b4079 100644 --- a/packages/app/src/i18n/ko.ts +++ b/packages/app/src/i18n/ko.ts @@ -500,6 +500,8 @@ export const dict = { "session.share.popover.description.unshared": "세션을 웹에 공개적으로 공유합니다. 링크가 있는 누구나 액세스할 수 있습니다.", "session.share.action.share": "공유", + "session.export.action": "스냅샷 내보내기", + "session.export.success": "세션 스냅샷을 내보냈습니다", "session.share.action.publish": "게시", "session.share.action.publishing": "게시 중...", "session.share.action.unpublish": "게시 취소", @@ -520,6 +522,7 @@ export const dict = { "common.closeTab": "탭 닫기", "common.dismiss": "닫기", "common.requestFailed": "요청 실패", + "common.active": "활성", "common.moreOptions": "더 많은 옵션", "common.learnMore": "더 알아보기", "common.rename": "이름 바꾸기", @@ -685,6 +688,14 @@ export const dict = { "settings.providers.section.connected": "연결된 공급자", "settings.providers.connected.empty": "연결된 공급자 없음", "settings.providers.section.popular": "인기 공급자", + "settings.providers.section.consoleAccount": "Console 계정", + "settings.providers.console.activeOrg": "활성 조직", + "settings.providers.console.noOrg": "선택된 조직 없음", + "settings.providers.console.switchOrg": "조직 전환", + "settings.providers.console.empty": "전환 가능한 Console 조직 없음", + "settings.providers.console.managedCount": "Console 관리 공급자 {{count}}개", + "settings.providers.console.switch.toast.title": "Console 조직 전환됨", + "settings.providers.console.switch.toast.description": "현재 {{org}} 사용 중", "settings.providers.tag.environment": "환경", "settings.providers.tag.config": "구성", "settings.providers.tag.custom": "사용자 지정", @@ -860,6 +871,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code(스냅샷)", + "settings.import.snapshot.file": "스냅샷 번들", + "settings.import.snapshot.file.hint": "내보낸 세션 스냅샷 번들(.json)을 선택하세요. 계속할 수 있는 새 세션으로 가져옵니다.", + "settings.import.snapshot.noFile": "먼저 스냅샷 번들 파일을 선택하세요", + "settings.import.snapshot.done": "스냅샷을 계속할 수 있는 새 세션으로 가져왔습니다", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/no.ts b/packages/app/src/i18n/no.ts index d0a87222..fe2a2e81 100644 --- a/packages/app/src/i18n/no.ts +++ b/packages/app/src/i18n/no.ts @@ -566,6 +566,8 @@ export const dict = { "session.share.popover.description.unshared": "Del sesjonen offentlig på nettet. Den vil være tilgjengelig for alle med lenken.", "session.share.action.share": "Del", + "session.export.action": "Eksporter snapshot", + "session.export.success": "Okt-snapshot eksportert", "session.share.action.publish": "Publiser", "session.share.action.publishing": "Publiserer...", "session.share.action.unpublish": "Avpubliser", @@ -589,6 +591,7 @@ export const dict = { "common.closeTab": "Lukk fane", "common.dismiss": "Avvis", "common.requestFailed": "Forespørsel mislyktes", + "common.active": "Aktiv", "common.moreOptions": "Flere alternativer", "common.learnMore": "Lær mer", "common.rename": "Gi nytt navn", @@ -766,6 +769,14 @@ export const dict = { "settings.providers.section.connected": "Tilkoblede leverandører", "settings.providers.connected.empty": "Ingen tilkoblede leverandører", "settings.providers.section.popular": "Populære leverandører", + "settings.providers.section.consoleAccount": "Console-konto", + "settings.providers.console.activeOrg": "Aktiv organisasjon", + "settings.providers.console.noOrg": "Ingen organisasjon valgt", + "settings.providers.console.switchOrg": "Bytt organisasjon", + "settings.providers.console.empty": "Ingen Console-organisasjoner å bytte mellom", + "settings.providers.console.managedCount": "{{count}} Console-administrerte leverandører", + "settings.providers.console.switch.toast.title": "Console-organisasjon byttet", + "settings.providers.console.switch.toast.description": "Bruker nå {{org}}", "settings.providers.tag.environment": "Miljø", "settings.providers.tag.config": "Konfigurasjon", "settings.providers.tag.custom": "Tilpasset", @@ -953,6 +964,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (snapshot)", + "settings.import.snapshot.file": "Snapshot-bundle", + "settings.import.snapshot.file.hint": "Velg en eksportert okt-snapshot-bundle (.json). Den importeres som en ny fortsettbar okt.", + "settings.import.snapshot.noFile": "Velg foerst en snapshot-bundle-fil", + "settings.import.snapshot.done": "Snapshot importert som en ny fortsettbar okt", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/pl.ts b/packages/app/src/i18n/pl.ts index ea9daff2..76ed195a 100644 --- a/packages/app/src/i18n/pl.ts +++ b/packages/app/src/i18n/pl.ts @@ -504,6 +504,8 @@ export const dict = { "session.share.popover.description.unshared": "Udostępnij sesję publicznie w sieci. Będzie dostępna dla każdego, kto posiada link.", "session.share.action.share": "Udostępnij", + "session.export.action": "Eksportuj migawke", + "session.export.success": "Migawka sesji wyeksportowana", "session.share.action.publish": "Opublikuj", "session.share.action.publishing": "Publikowanie...", "session.share.action.unpublish": "Cofnij publikację", @@ -524,6 +526,7 @@ export const dict = { "common.closeTab": "Zamknij kartę", "common.dismiss": "Odrzuć", "common.requestFailed": "Żądanie nie powiodło się", + "common.active": "Aktywna", "common.moreOptions": "Więcej opcji", "common.learnMore": "Dowiedz się więcej", "common.rename": "Zmień nazwę", @@ -693,6 +696,14 @@ export const dict = { "settings.providers.section.connected": "Połączeni dostawcy", "settings.providers.connected.empty": "Brak połączonych dostawców", "settings.providers.section.popular": "Popularni dostawcy", + "settings.providers.section.consoleAccount": "Konto Console", + "settings.providers.console.activeOrg": "Aktywna organizacja", + "settings.providers.console.noOrg": "Nie wybrano organizacji", + "settings.providers.console.switchOrg": "Przełącz organizację", + "settings.providers.console.empty": "Brak organizacji Console do przełączenia", + "settings.providers.console.managedCount": "{{count}} dostawców zarządzanych przez Console", + "settings.providers.console.switch.toast.title": "Przełączono organizację Console", + "settings.providers.console.switch.toast.description": "Teraz używany jest {{org}}", "settings.providers.tag.environment": "Środowisko", "settings.providers.tag.config": "Konfiguracja", "settings.providers.tag.custom": "Niestandardowe", @@ -870,6 +881,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (migawka)", + "settings.import.snapshot.file": "Pakiet migawki", + "settings.import.snapshot.file.hint": "Wybierz wyeksportowany pakiet migawki sesji (.json). Zostanie zaimportowany jako nowa kontynuowalna sesja.", + "settings.import.snapshot.noFile": "Najpierw wybierz plik pakietu migawki", + "settings.import.snapshot.done": "Migawka zaimportowana jako nowa kontynuowalna sesja", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/ru.ts b/packages/app/src/i18n/ru.ts index 7e599757..20609699 100644 --- a/packages/app/src/i18n/ru.ts +++ b/packages/app/src/i18n/ru.ts @@ -562,6 +562,8 @@ export const dict = { "session.share.popover.description.unshared": "Опубликуйте сессию в интернете. Доступ к ней сможет получить любой, у кого есть ссылка.", "session.share.action.share": "Поделиться", + "session.export.action": "Экспортировать снапшот", + "session.export.success": "Снапшот сессии экспортирован", "session.share.action.publish": "Опубликовать", "session.share.action.publishing": "Публикация...", "session.share.action.unpublish": "Отменить публикацию", @@ -585,6 +587,7 @@ export const dict = { "common.closeTab": "Закрыть вкладку", "common.dismiss": "Закрыть", "common.requestFailed": "Запрос не выполнен", + "common.active": "Активна", "common.moreOptions": "Дополнительные опции", "common.learnMore": "Подробнее", "common.rename": "Переименовать", @@ -767,6 +770,14 @@ export const dict = { "settings.providers.section.connected": "Подключённые провайдеры", "settings.providers.connected.empty": "Нет подключённых провайдеров", "settings.providers.section.popular": "Популярные провайдеры", + "settings.providers.section.consoleAccount": "Аккаунт Console", + "settings.providers.console.activeOrg": "Активная организация", + "settings.providers.console.noOrg": "Организация не выбрана", + "settings.providers.console.switchOrg": "Сменить организацию", + "settings.providers.console.empty": "Нет доступных для переключения организаций Console", + "settings.providers.console.managedCount": "Провайдеров под управлением Console: {{count}}", + "settings.providers.console.switch.toast.title": "Организация Console изменена", + "settings.providers.console.switch.toast.description": "Теперь используется {{org}}", "settings.providers.tag.environment": "Среда", "settings.providers.tag.config": "Конфигурация", "settings.providers.tag.custom": "Пользовательский", @@ -950,6 +961,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (снапшот)", + "settings.import.snapshot.file": "Пакет снапшота", + "settings.import.snapshot.file.hint": "Выберите экспортированный пакет снапшота сессии (.json). Он будет импортирован как новая возобновляемая сессия.", + "settings.import.snapshot.noFile": "Сначала выберите файл пакета снапшота", + "settings.import.snapshot.done": "Снапшот импортирован как новая возобновляемая сессия", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/th.ts b/packages/app/src/i18n/th.ts index 8cea2edd..d156dc80 100644 --- a/packages/app/src/i18n/th.ts +++ b/packages/app/src/i18n/th.ts @@ -558,6 +558,8 @@ export const dict = { "session.share.popover.description.shared": "เซสชันนี้เป็นสาธารณะบนเว็บ สามารถเข้าถึงได้โดยผู้ที่มีลิงก์", "session.share.popover.description.unshared": "แชร์เซสชันสาธารณะบนเว็บ จะเข้าถึงได้โดยผู้ที่มีลิงก์", "session.share.action.share": "แชร์", + "session.export.action": "ส่งออกสแนปชอต", + "session.export.success": "ส่งออกสแนปชอตเซสชันแล้ว", "session.share.action.publish": "เผยแพร่", "session.share.action.publishing": "กำลังเผยแพร่...", "session.share.action.unpublish": "ยกเลิกการเผยแพร่", @@ -580,6 +582,7 @@ export const dict = { "common.closeTab": "ปิดแท็บ", "common.dismiss": "ปิด", "common.requestFailed": "คำขอล้มเหลว", + "common.active": "ใช้งานอยู่", "common.moreOptions": "ตัวเลือกเพิ่มเติม", "common.learnMore": "เรียนรู้เพิ่มเติม", "common.rename": "เปลี่ยนชื่อ", @@ -756,6 +759,14 @@ export const dict = { "settings.providers.section.connected": "ผู้ให้บริการที่เชื่อมต่อ", "settings.providers.connected.empty": "ไม่มีผู้ให้บริการที่เชื่อมต่อ", "settings.providers.section.popular": "ผู้ให้บริการยอดนิยม", + "settings.providers.section.consoleAccount": "บัญชี Console", + "settings.providers.console.activeOrg": "องค์กรที่ใช้งาน", + "settings.providers.console.noOrg": "ยังไม่ได้เลือกองค์กร", + "settings.providers.console.switchOrg": "สลับองค์กร", + "settings.providers.console.empty": "ไม่มีองค์กร Console ที่สลับได้", + "settings.providers.console.managedCount": "ผู้ให้บริการที่จัดการโดย Console {{count}} ราย", + "settings.providers.console.switch.toast.title": "สลับองค์กร Console แล้ว", + "settings.providers.console.switch.toast.description": "กำลังใช้ {{org}}", "settings.providers.tag.environment": "สภาพแวดล้อม", "settings.providers.tag.config": "กำหนดค่า", "settings.providers.tag.custom": "กำหนดเอง", @@ -937,6 +948,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (สแนปชอต)", + "settings.import.snapshot.file": "ชุดสแนปชอต", + "settings.import.snapshot.file.hint": "เลือกชุดสแนปชอตเซสชันที่ส่งออก (.json) จะนำเข้าเป็นเซสชันใหม่ที่ทำงานต่อได้", + "settings.import.snapshot.noFile": "เลือกไฟล์ชุดสแนปชอตก่อน", + "settings.import.snapshot.done": "นำเข้าสแนปชอตเป็นเซสชันใหม่ที่ทำงานต่อแล้ว", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/tr.ts b/packages/app/src/i18n/tr.ts index e4abc007..51ad08cb 100644 --- a/packages/app/src/i18n/tr.ts +++ b/packages/app/src/i18n/tr.ts @@ -568,6 +568,8 @@ export const dict = { "session.share.popover.description.unshared": "Oturumu web'de herkese açık olarak paylaşın. Bağlantıya sahip herkes erişebilecek.", "session.share.action.share": "Paylaş", + "session.export.action": "Anlık goruntuyu disa aktar", + "session.export.success": "Oturum anlık goruntusu disa aktarıldı", "session.share.action.publish": "Yayınla", "session.share.action.publishing": "Yayınlanıyor...", "session.share.action.unpublish": "Yayından kaldır", @@ -591,6 +593,7 @@ export const dict = { "common.closeTab": "Sekmeyi kapat", "common.dismiss": "Kapat", "common.requestFailed": "İstek başarısız", + "common.active": "Etkin", "common.moreOptions": "Daha fazla seçenek", "common.learnMore": "Daha fazla bilgi", "common.rename": "Yeniden adlandır", @@ -775,6 +778,14 @@ export const dict = { "settings.providers.section.connected": "Bağlı sağlayıcılar", "settings.providers.connected.empty": "Bağlı sağlayıcı yok", "settings.providers.section.popular": "Popüler sağlayıcılar", + "settings.providers.section.consoleAccount": "Console hesabı", + "settings.providers.console.activeOrg": "Etkin kuruluş", + "settings.providers.console.noOrg": "Kuruluş seçilmedi", + "settings.providers.console.switchOrg": "Kuruluş değiştir", + "settings.providers.console.empty": "Değiştirilebilir Console kuruluşu yok", + "settings.providers.console.managedCount": "Console tarafından yönetilen {{count}} sağlayıcı", + "settings.providers.console.switch.toast.title": "Console kuruluşu değiştirildi", + "settings.providers.console.switch.toast.description": "Şimdi {{org}} kullanılıyor", "settings.providers.tag.environment": "Ortam", "settings.providers.tag.config": "Yapılandırma", "settings.providers.tag.custom": "Özel", @@ -960,6 +971,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (anlık goruntu)", + "settings.import.snapshot.file": "Anlık goruntu paketi", + "settings.import.snapshot.file.hint": "Disa aktarılmıs bir oturum anlık goruntu paketi (.json) secin. Devam edilebilir yeni bir oturum olarak ice aktarılır.", + "settings.import.snapshot.noFile": "Once bir anlık goruntu paketi dosyası secin", + "settings.import.snapshot.done": "Anlık goruntu devam edilebilir yeni bir oturum olarak ice aktarıldı", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/uk.ts b/packages/app/src/i18n/uk.ts index 7a82b2a5..b8c8d2d9 100644 --- a/packages/app/src/i18n/uk.ts +++ b/packages/app/src/i18n/uk.ts @@ -616,6 +616,8 @@ export const dict = { "session.share.popover.description.unshared": "Опублікуйте сесію публічно в інтернеті. Вона буде доступна будь-кому за посиланням.", "session.share.action.share": "Поділитися", + "session.export.action": "Експортувати знімок", + "session.export.success": "Знімок сеансу експортовано", "session.share.action.publish": "Опублікувати", "session.share.action.publishing": "Публікація...", "session.share.action.unpublish": "Скасувати публікацію", @@ -641,6 +643,7 @@ export const dict = { "common.dismiss": "Відхилити", "common.moreCountSuffix": " (ще {{count}})", "common.requestFailed": "Запит не виконано", + "common.active": "Активна", "common.moreOptions": "Більше опцій", "common.learnMore": "Дізнатися більше", "common.rename": "Перейменувати", @@ -887,6 +890,14 @@ export const dict = { "settings.providers.connected.empty": "Немає підключених провайдерів", "settings.providers.connected.environmentDescription": "Підключено зі змінних середовища", "settings.providers.section.popular": "Популярні провайдери", + "settings.providers.section.consoleAccount": "Обліковий запис Console", + "settings.providers.console.activeOrg": "Активна організація", + "settings.providers.console.noOrg": "Організацію не вибрано", + "settings.providers.console.switchOrg": "Змінити організацію", + "settings.providers.console.empty": "Немає організацій Console для перемикання", + "settings.providers.console.managedCount": "Провайдерів під керуванням Console: {{count}}", + "settings.providers.console.switch.toast.title": "Організацію Console змінено", + "settings.providers.console.switch.toast.description": "Тепер використовується {{org}}", "settings.providers.custom.description": "Додайте провайдера, сумісного з OpenAI, за базовим URL.", "settings.providers.tag.environment": "Середовище", "settings.providers.tag.config": "Конфігурація", @@ -981,6 +992,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "Custom", + "settings.import.preset.deepagent": "DeepAgent Code (знімок)", + "settings.import.snapshot.file": "Пакет знімка", + "settings.import.snapshot.file.hint": "Виберіть експортований пакет знімка сеансу (.json). Його буде імпортовано як нову продовжувану сесію.", + "settings.import.snapshot.noFile": "Спочатку виберіть файл пакета знімка", + "settings.import.snapshot.done": "Знімок імпортовано як нову продовжувану сесію", "settings.import.customPath": "Path", "settings.import.customPath.hint": "Absolute path to the agent data directory.", "settings.import.scope": "What to import", diff --git a/packages/app/src/i18n/zh.ts b/packages/app/src/i18n/zh.ts index 7baf438a..955a4e55 100644 --- a/packages/app/src/i18n/zh.ts +++ b/packages/app/src/i18n/zh.ts @@ -894,6 +894,8 @@ export const dict = { "session.share.popover.description.shared": "此会话已在网页上公开。任何拥有链接的人都可以访问。", "session.share.popover.description.unshared": "在网页上公开分享此会话。任何拥有链接的人都可以访问。", "session.share.action.share": "分享", + "session.export.action": "导出快照", + "session.export.success": "会话快照已导出", "session.share.action.publish": "发布", "session.share.action.publishing": "正在发布...", "session.share.action.unpublish": "取消发布", @@ -929,6 +931,7 @@ export const dict = { "common.closeTab": "关闭标签页", "common.dismiss": "忽略", "common.requestFailed": "请求失败", + "common.active": "当前", "common.moreOptions": "更多选项", "common.learnMore": "了解更多", "common.rename": "重命名", @@ -1203,6 +1206,14 @@ export const dict = { "settings.providers.section.connected": "已连接的提供商", "settings.providers.connected.empty": "没有已连接的提供商", "settings.providers.section.popular": "热门提供商", + "settings.providers.section.consoleAccount": "Console 账号", + "settings.providers.console.activeOrg": "当前组织", + "settings.providers.console.noOrg": "未选择组织", + "settings.providers.console.switchOrg": "切换组织", + "settings.providers.console.empty": "没有可切换的 Console 组织", + "settings.providers.console.managedCount": "{{count}} 个 Console 托管的提供商", + "settings.providers.console.switch.toast.title": "已切换 Console 组织", + "settings.providers.console.switch.toast.description": "当前使用 {{org}}", "settings.providers.tag.environment": "环境", "settings.providers.tag.config": "配置", "settings.providers.tag.custom": "自定义", @@ -1403,6 +1414,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "自定义", + "settings.import.preset.deepagent": "DeepAgent Code(快照)", + "settings.import.snapshot.file": "快照包", + "settings.import.snapshot.file.hint": "选择导出的会话快照包(.json),将导入为新的可续跑会话。", + "settings.import.snapshot.noFile": "请先选择快照包文件", + "settings.import.snapshot.done": "快照已导入为新的可续跑会话", "settings.import.customPath": "路径", "settings.import.customPath.hint": "Agent 数据目录的绝对路径。", "settings.import.scope": "导入内容", @@ -1543,6 +1559,22 @@ export const dict = { "session.context.copyRef": "复制稳定引用", "session.context.provenance": "来源", "session.context.relations": "关联", + "session.stats.title": "用量与成本", + "session.stats.overview": "概览", + "session.stats.sessions": "会话数", + "session.stats.days": "天数", + "session.stats.costTokens": "成本与 Token", + "session.stats.totalCost": "总成本", + "session.stats.costPerDay": "日均成本", + "session.stats.tokensPerSession": "平均 Token/会话", + "session.stats.medianTokens": "中位 Token/会话", + "session.stats.input": "输入", + "session.stats.output": "输出", + "session.stats.reasoning": "推理", + "session.stats.cacheRead": "缓存读取", + "session.stats.cacheWrite": "缓存写入", + "session.stats.modelUsage": "模型用量", + "session.stats.noData": "暂无会话用量数据。", "session.context.summary.complete": "完整", "session.context.summary.partial": "部分", "session.context.summary.empty": "空", diff --git a/packages/app/src/i18n/zht.ts b/packages/app/src/i18n/zht.ts index 745deeb7..51f35cda 100644 --- a/packages/app/src/i18n/zht.ts +++ b/packages/app/src/i18n/zht.ts @@ -699,6 +699,8 @@ export const dict = { "session.share.popover.description.shared": "此工作階段已在網頁上公開。任何擁有連結的人都可以存取。", "session.share.popover.description.unshared": "在網頁上公開分享此工作階段。任何擁有連結的人都可以存取。", "session.share.action.share": "分享", + "session.export.action": "匯出快照", + "session.export.success": "工作階段快照已匯出", "session.share.action.publish": "發佈", "session.share.action.publishing": "正在發佈...", "session.share.action.unpublish": "取消發佈", @@ -721,6 +723,7 @@ export const dict = { "common.closeTab": "關閉標籤頁", "common.dismiss": "忽略", "common.requestFailed": "要求失敗", + "common.active": "目前", "common.moreOptions": "更多選項", "common.learnMore": "深入了解", "common.rename": "重新命名", @@ -920,6 +923,14 @@ export const dict = { "settings.providers.section.connected": "已連線的提供商", "settings.providers.connected.empty": "沒有已連線的提供商", "settings.providers.section.popular": "熱門提供商", + "settings.providers.section.consoleAccount": "Console 帳號", + "settings.providers.console.activeOrg": "目前組織", + "settings.providers.console.noOrg": "未選擇組織", + "settings.providers.console.switchOrg": "切換組織", + "settings.providers.console.empty": "沒有可切換的 Console 組織", + "settings.providers.console.managedCount": "{{count}} 個 Console 託管的供應商", + "settings.providers.console.switch.toast.title": "已切換 Console 組織", + "settings.providers.console.switch.toast.description": "目前使用 {{org}}", "settings.providers.tag.environment": "環境", "settings.providers.tag.config": "配置", "settings.providers.tag.custom": "自訂", @@ -1104,6 +1115,11 @@ export const dict = { "settings.import.preset.codex": "Codex", "settings.import.preset.claude": "Claude Code", "settings.import.preset.custom": "自訂", + "settings.import.preset.deepagent": "DeepAgent Code(快照)", + "settings.import.snapshot.file": "快照包", + "settings.import.snapshot.file.hint": "選擇匯出的工作階段快照包(.json),將匯入為新的可接續工作階段。", + "settings.import.snapshot.noFile": "請先選擇快照包檔案", + "settings.import.snapshot.done": "快照已匯入為新的可接續工作階段", "settings.import.customPath": "路徑", "settings.import.customPath.hint": "Agent 資料目錄的絕對路徑。", "settings.import.scope": "匯入內容", diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 96f2616c..259c20e9 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -74,6 +74,7 @@ import { useSessionCommands } from "@/pages/session/use-session-commands" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" import { Identifier } from "@/utils/id" import { diffs as list } from "@/utils/diffs" +import { messageOrderKey } from "@/utils/message-order" import { Persist, persisted } from "@/utils/persist" import { extractPromptFromParts } from "@/utils/prompt" import { same } from "@/utils/same" @@ -384,6 +385,14 @@ export default function Page() { () => { const revert = revertMessageID() if (!revert) return userMessages() + // BUG-407-012 root cause B: the revert boundary is the TARGET message's time, not its ID — + // ID lexicographic order flips across the 6-byte ID time wrap. If the target is not in the + // loaded window, degrade to the legacy ID boundary instead of guessing a time. + const target = messages().find((m) => m.id === revert) + if (target) { + const bound = messageOrderKey(target) + return userMessages().filter((m) => messageOrderKey(m) < bound) + } return userMessages().filter((m) => m.id < revert) }, emptyUserMessages, @@ -391,6 +400,8 @@ export default function Page() { equals: same, }, ) + // The timeline arrays are maintained in (time, id) chronological order, so the last element is + // the latest message even when its wrapped `msg_00...` ID sorts before older `msg_ff...` IDs. const lastUserMessage = createMemo(() => visibleUserMessages().at(-1)) createEffect(() => { @@ -718,9 +729,14 @@ export default function Page() { createEffect( on( - () => visibleUserMessages().at(-1)?.id, - (lastId, prevLastId) => { - if (lastId && prevLastId && lastId > prevLastId) { + // BUG-407-012 root cause B: compare (time, id) order keys, not raw IDs — a wrapped + // `msg_00...` newest message is lexicographically smaller than the previous `msg_ff...` one. + () => { + const last = visibleUserMessages().at(-1) + return last ? messageOrderKey(last) : undefined + }, + (lastOrder, prevOrder) => { + if (lastOrder && prevOrder && lastOrder > prevOrder) { setStore("messageId", undefined) } }, diff --git a/packages/app/src/pages/session/message-timeline-virtualize.test.ts b/packages/app/src/pages/session/message-timeline-virtualize.test.ts new file mode 100644 index 00000000..5a60fac9 --- /dev/null +++ b/packages/app/src/pages/session/message-timeline-virtualize.test.ts @@ -0,0 +1,172 @@ +import { afterAll, beforeAll, describe, expect, mock, test } from "bun:test" +import type { Part, ToolPart, ToolStateCompleted } from "@deepagent-code/sdk/v2" + +let shouldVirtualizeToolDiff: typeof import("./message-timeline").shouldVirtualizeToolDiff +let toolDiffPayloadSize: typeof import("./message-timeline").toolDiffPayloadSize + +beforeAll(async () => { + // Bun test resolves solid-js through the "worker" condition (SSR build). Several + // packages in message-timeline's import graph assume the client build at module scope: + // virtua/solid imports a web-only export, and @solidjs/router / @kobalte/core chunks + // call client-only solid APIs on load. Stub those packages (nothing renders here, the + // functions under test are pure) while keeping solid-js itself on the single SSR build + // used by every other test file - mixing client/server solid builds in one process + // deadlocks the reactive scheduler. + const componentStub: any = new Proxy(() => null, { get: () => componentStub }) + mock.module("virtua/solid", () => ({ Virtualizer: componentStub })) + // Keep the real (Kobalte-backed) message-part out of the module cache: other tests + // (message-timeline.data.test.ts) mock this module and bun re-links cached importers + // against the real one otherwise. + mock.module("@deepagent-code/ui/message-part", () => ({ + ContextToolGroup: componentStub, + Message: componentStub, + MessageDivider: componentStub, + Part: componentStub, + partDefaultOpen: () => false, + groupParts: () => [], + renderable: () => true, + })) + mock.module("@solidjs/router", () => ({ + A: componentStub, + Navigate: componentStub, + Route: componentStub, + Router: componentStub, + useIsRouting: () => () => false, + useLocation: () => ({ pathname: "/" }), + useNavigate: () => () => undefined, + useParams: () => ({}), + useSearchParams: () => ({}), + })) + const kobalte: Record> = { + "@kobalte/core/accordion": { Accordion: componentStub }, + "@kobalte/core/button": { Button: componentStub }, + "@kobalte/core/checkbox": { Checkbox: componentStub }, + "@kobalte/core/collapsible": { Collapsible: componentStub }, + "@kobalte/core/context-menu": { ContextMenu: componentStub }, + "@kobalte/core/dialog": { Dialog: componentStub }, + "@kobalte/core/dropdown-menu": { DropdownMenu: componentStub }, + "@kobalte/core/hover-card": { HoverCard: componentStub }, + "@kobalte/core/popover": { Popover: componentStub }, + "@kobalte/core/progress": { Progress: componentStub }, + "@kobalte/core/radio-group": { RadioGroup: componentStub }, + "@kobalte/core/segmented-control": { SegmentedControl: componentStub }, + "@kobalte/core/select": { Select: componentStub }, + "@kobalte/core/switch": { Switch: componentStub }, + "@kobalte/core/tabs": { Tabs: componentStub }, + "@kobalte/core/text-field": { TextField: componentStub }, + "@kobalte/core/toast": { Toast: componentStub, toaster: componentStub }, + "@kobalte/core/tooltip": { Tooltip: componentStub }, + } + for (const [name, exports] of Object.entries(kobalte)) { + mock.module(name, () => exports) + } + + const mod = await import("./message-timeline") + shouldVirtualizeToolDiff = mod.shouldVirtualizeToolDiff + toolDiffPayloadSize = mod.toolDiffPayloadSize +}) + +// Drop the stubs once this file's tests are done so later files link the real modules; +// the modules loaded above stay cached but are harmless (other test files that need +// them, e.g. message-timeline.data.test.ts, register their own mocks). +afterAll(() => mock.restore()) + +const large = "x".repeat(500_001) +const small = "x".repeat(100) + +function completedTool(overrides: Partial> = {}, tool = "edit"): ToolPart { + return { + id: "part-1", + sessionID: "session-1", + messageID: "message-1", + type: "tool", + callID: "call-1", + tool, + state: { + status: "completed", + input: {}, + output: small, + title: "done", + metadata: {}, + time: { start: 1, end: 2 }, + ...overrides, + }, + } +} + +describe("shouldVirtualizeToolDiff (UX-001)", () => { + test("non-tool parts never virtualize", () => { + const part = { + id: "part-1", + sessionID: "session-1", + messageID: "message-1", + type: "text", + text: large, + } satisfies Part + expect(toolDiffPayloadSize(part)).toBe(0) + expect(shouldVirtualizeToolDiff(part)).toBe(false) + }) + + test("streaming (pending/running) parts never virtualize even with large payloads", () => { + const running: ToolPart = { + id: "part-1", + sessionID: "session-1", + messageID: "message-1", + type: "tool", + callID: "call-1", + tool: "edit", + state: { status: "running", input: {}, metadata: { filediff: { after: large } }, time: { start: 1 } }, + } + expect(shouldVirtualizeToolDiff(running)).toBe(false) + + const pending: ToolPart = { ...running, state: { status: "pending", input: {}, raw: large } } + expect(shouldVirtualizeToolDiff(pending)).toBe(false) + }) + + test("error parts never virtualize", () => { + const errored: ToolPart = { + id: "part-1", + sessionID: "session-1", + messageID: "message-1", + type: "tool", + callID: "call-1", + tool: "edit", + state: { status: "error", input: {}, error: large, time: { start: 1, end: 2 } }, + } + expect(shouldVirtualizeToolDiff(errored)).toBe(false) + }) + + test("small completed diffs stay non-virtualized", () => { + expect(shouldVirtualizeToolDiff(completedTool())).toBe(false) + expect(shouldVirtualizeToolDiff(completedTool({ output: small, metadata: { filediff: { after: small } } }))).toBe( + false, + ) + }) + + test("completed edit tool with large filediff payload virtualizes", () => { + expect(shouldVirtualizeToolDiff(completedTool({ metadata: { filediff: { after: large } } }))).toBe(true) + expect(shouldVirtualizeToolDiff(completedTool({ metadata: { filediff: { before: large } } }))).toBe(true) + expect(shouldVirtualizeToolDiff(completedTool({ metadata: { filediff: { patch: large } } }))).toBe(true) + }) + + test("completed apply_patch with large per-file patch virtualizes", () => { + const part = completedTool({ metadata: { files: [{ filePath: "a.ts", patch: large }] } }, "apply_patch") + expect(shouldVirtualizeToolDiff(part)).toBe(true) + + const multi = completedTool( + { metadata: { files: [{ filePath: "a.ts", patch: small }, { filePath: "b.ts", diff: large }] } }, + "apply_patch", + ) + expect(shouldVirtualizeToolDiff(multi)).toBe(true) + }) + + test("falls back to output length proxy when no diff metadata exists", () => { + expect(shouldVirtualizeToolDiff(completedTool({ output: large }))).toBe(true) + expect(toolDiffPayloadSize(completedTool({ output: large }))).toBe(large.length) + }) + + test("threshold boundary is strict (> 500KB)", () => { + const exact = completedTool({ output: "x".repeat(500_000) }) + expect(shouldVirtualizeToolDiff(exact)).toBe(false) + }) +}) diff --git a/packages/app/src/pages/session/message-timeline.bug407008.test.ts b/packages/app/src/pages/session/message-timeline.bug407008.test.ts new file mode 100644 index 00000000..24b581ac --- /dev/null +++ b/packages/app/src/pages/session/message-timeline.bug407008.test.ts @@ -0,0 +1,369 @@ +/** + * BUG-407-008 — spec §7.2 App projection deterministic regression tests (scenarios 18-29, + * timeline-layer subset: 18, 19, 26, 27, 28, 29). + * + * Harness/fixture style mirrors ./message-timeline.data.test.ts (same `@deepagent-code/ui/message-part` + * mock, same `Timeline.constructMessageRows` invocation shape). + * + * Fix-E (spec §5 Fix-E — per-Activity latest-revision convergence) is RESTORED here: commit + * 1ae96499 ("fix(app): preserve activity progress history") had removed it, but the adjudicated + * decision is to converge to the latest revision (hide superseded older revisions). These tests + * assert the spec-compliant convergence behavior. + */ +import { afterAll, describe, expect, mock, spyOn, test } from "bun:test" +import type { AssistantMessage, Part, UserMessage } from "@deepagent-code/sdk/v2/client" +import type { TimelineRow } from "./message-timeline.data" + +mock.module("@deepagent-code/ui/message-part", () => ({ + groupParts: (refs: { messageID: string; part: Part }[]) => + refs.map((item) => ({ + key: `part:${item.messageID}:${item.part.id}`, + type: "part", + ref: { messageID: item.messageID, partID: item.part.id }, + })), + renderable: (part: Part, showReasoningSummaries = true) => + part.type !== "reasoning" || showReasoningSummaries, +})) + +afterAll(() => mock.restore()) + +// --------------------------------------------------------------------------- +// Precise incident fixture (docs/bug-407-008.md §2.1/§2.2): +// activity a6d06b2a...c5e5a9 +// revision 8 msg_ff200fab7001lSTuDLJcykJfn6 reasoning prt_ff2010818001nSfs68M6HUBq2E +// + tool call tool_sd47zQa3WQeOSa1BKPhv6mF1 (bash, ordinal 548) +// revision 9 msg_ff2015000001RBapgqkQdqoqQI reasoning prt_ff2015c8e00132UPp8LegJCQQ0 +// + tool call tool_ZSTCdihDT0jY74tJVeAZLZ45 (bash, ordinal 549) +// revision 10 msg_ff2019410001interrupted zero renderable parts, state=interrupted +// Both reasoning texts are 1,668 chars and byte-identical (SHA-256 5bc40233...d37b). +// --------------------------------------------------------------------------- + +const INCIDENT_ACTIVITY = "a6d06b2a82ef234ab9dc71e3fd940292a21b34f4732f28aa19ba8416c4c5e5a9" +// Representative verbatim-identical reasoning body (real incident text is 1,668 chars). +const INCIDENT_REASONING = `Check the stress log tail and whether w70_stress is still running. ${"user0/user1/user3 FINAL observed; user2 FINAL still missing. ".repeat(20)}`.trim() + +const user = { + id: "msg_user_incident", + sessionID: "ses_0149b8afffffWlu80cVGdzFI9s", + role: "user", + agent: "build", + model: { providerID: "deepseek", modelID: "deepseek-chat" }, + time: { created: 1 }, +} as UserMessage + +const assistant = (id: string, activityProgress?: AssistantMessage["activityProgress"]) => + ({ + id, + sessionID: user.sessionID, + parentID: user.id, + role: "assistant", + mode: "build", + agent: "build", + modelID: "deepseek-chat", + providerID: "deepseek", + path: { cwd: "/project", root: "/project" }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 1, completed: 2 }, + finish: "tool-calls", + activityProgress, + }) as AssistantMessage + +const reasoningPart = (messageID: string, id: string, text: string) => + ({ id, sessionID: user.sessionID, messageID, type: "reasoning", text }) as Part + +const toolPart = (messageID: string, id: string, callID: string, command: string) => + ({ + id, + sessionID: user.sessionID, + messageID, + type: "tool", + callID, + tool: "bash", + state: { + status: "completed", + input: { command }, + output: command, + title: command, + metadata: {}, + time: { start: 1, end: 2 }, + }, + }) as Part + +const legacyMarkerPart = ( + messageID: string, + id: string, + text: string, + marker: { activity_id: string; revision: number; state: string }, +) => + ({ + id, + sessionID: user.sessionID, + messageID, + type: "text", + text, + metadata: { deepagent_activity_progress: marker }, + }) as Part + +const partIDs = (rows: TimelineRow.TimelineRow[]): string[] => + rows.flatMap((row) => (row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [])) + +describe("BUG-407-008 §7.2 App projection — incident fixture (timeline layer)", () => { + test("18: precise incident fixture — revision 8/9 reasoning+tool with identical reasoning", async () => { + const { Timeline } = await import("./message-timeline.data") + // Spec §7.2 (18): with Fix-E restored, the App shows ONLY the latest revision's (9) renderable + // parts; the superseded revision 8 (identical reasoning + earlier tool) is hidden. + const msg8 = assistant("msg_ff200fab7001lSTuDLJcykJfn6", { + activityID: INCIDENT_ACTIVITY, + revision: 8, + state: "progress", + }) + const msg9 = assistant("msg_ff2015000001RBapgqkQdqoqQI", { + activityID: INCIDENT_ACTIVITY, + revision: 9, + state: "progress", + }) + const parts = new Map([ + [ + msg8.id, + [ + reasoningPart(msg8.id, "prt_ff2010818001nSfs68M6HUBq2E", INCIDENT_REASONING), + toolPart(msg8.id, "prt_tool_rev8", "tool_sd47zQa3WQeOSa1BKPhv6mF1", "tail stress.log; pgrep w70_stress"), + ], + ], + [ + msg9.id, + [ + // Verbatim-identical reasoning text (SHA-256 match in the real incident). + reasoningPart(msg9.id, "prt_ff2015c8e00132UPp8LegJCQQ0", INCIDENT_REASONING), + toolPart(msg9.id, "prt_tool_rev9", "tool_ZSTCdihDT0jY74tJVeAZLZ45", "tail stress.log; wait user2 FINAL"), + ], + ], + ]) + + const rows = Timeline.constructMessageRows(user, (id) => parts.get(id) ?? [], [msg8, msg9], 0, true, "idle", false) + const visible = partIDs(rows) + + // Only the latest revision (9) renders; superseded revision 8 is hidden. + expect(visible).toContain("prt_ff2015c8e00132UPp8LegJCQQ0") + expect(visible).toContain("prt_tool_rev9") + expect(visible).not.toContain("prt_ff2010818001nSfs68M6HUBq2E") + expect(visible).not.toContain("prt_tool_rev8") + expect(visible).toEqual(["prt_ff2015c8e00132UPp8LegJCQQ0", "prt_tool_rev9"]) + }) + + test("19: terminal revision 10 has zero parts and state=interrupted", async () => { + const { Timeline } = await import("./message-timeline.data") + // Spec §7.2 (19): with Fix-E restored, once the zero-part terminal revision 10 (interrupted) + // arrives, it supersedes revisions 8/9 (terminal beats non-terminal), so 8/9 are fully hidden; + // the zero-part terminal contributes no part rows of its own. The interrupted TurnDivider + // contract stays driven by MessageAbortedError (existing UI preserved). + const msg8 = assistant("msg_ff200fab7001lSTuDLJcykJfn6", { + activityID: INCIDENT_ACTIVITY, + revision: 8, + state: "progress", + }) + const msg9 = assistant("msg_ff2015000001RBapgqkQdqoqQI", { + activityID: INCIDENT_ACTIVITY, + revision: 9, + state: "progress", + }) + const msg10 = assistant("msg_ff2019410001interrupted", { + activityID: INCIDENT_ACTIVITY, + revision: 10, + state: "interrupted", + terminalReason: "user_abort", + }) + const parts = new Map([ + [ + msg8.id, + [ + reasoningPart(msg8.id, "prt_ff2010818001nSfs68M6HUBq2E", INCIDENT_REASONING), + toolPart(msg8.id, "prt_tool_rev8", "tool_sd47zQa3WQeOSa1BKPhv6mF1", "tail stress.log"), + ], + ], + [ + msg9.id, + [ + reasoningPart(msg9.id, "prt_ff2015c8e00132UPp8LegJCQQ0", INCIDENT_REASONING), + toolPart(msg9.id, "prt_tool_rev9", "tool_ZSTCdihDT0jY74tJVeAZLZ45", "tail stress.log again"), + ], + ], + // revision 10: zero parts on purpose (provider-backed interrupted row). + [msg10.id, []], + ]) + + const rows = Timeline.constructMessageRows( + user, + (id) => parts.get(id) ?? [], + [msg8, msg9, msg10], + 0, + true, + "idle", + false, + ) + const visible = partIDs(rows) + + // Revision 8/9 are hidden once the terminal revision 10 arrives; nothing renderable remains. + expect(visible).toEqual([]) + // The zero-part terminal message contributes no part rows ... + expect(rows.some((row) => row._tag === "AssistantPart" && row.group.key.includes(msg10.id))).toBe(false) + // ... and (without a MessageAbortedError) no extra "interrupted" divider is synthesized: + // the existing interrupted UI contract is driven solely by MessageAbortedError. + expect(rows.filter((row) => row._tag === "TurnDivider" && row.label === "interrupted")).toEqual([]) + }) + + test("26: a message with multiple reasoning/text/tool parts is shown or hidden as a whole group", async () => { + const { Timeline } = await import("./message-timeline.data") + // DEV-407008-E3: spec §7.2 (26) requires whole-group show/hide and forbids hiding only the + // part whose text matches a marker. Fix-E removal means NOTHING is hidden today, which + // trivially satisfies "no partial hiding": every part of the group is rendered together. + const message = assistant("msg_group", { + activityID: "activity-group", + revision: 3, + state: "progress", + }) + const parts = new Map([ + [ + message.id, + [ + reasoningPart(message.id, "prt_group_reasoning", INCIDENT_REASONING), + legacyMarkerPart(message.id, "prt_group_marker", `revision 3`, { + activity_id: "activity-group", + revision: 3, + state: "progress", + }), + { + id: "prt_group_text", + sessionID: user.sessionID, + messageID: message.id, + type: "text", + text: INCIDENT_REASONING, // same body as the reasoning part — must NOT be singled out + } as Part, + toolPart(message.id, "prt_group_tool", "call_group", "ls -la"), + ], + ], + ]) + + const rows = Timeline.constructMessageRows(user, (id) => parts.get(id) ?? [], [message], 0, true, "idle", false) + const visible = partIDs(rows) + + // Whole-group visibility: all four parts render; no part is individually hidden. + expect(visible).toEqual(["prt_group_reasoning", "prt_group_marker", "prt_group_text", "prt_group_tool"]) + }) + + test("27: identical reasoning text in two different Activities shows in both (no cross-activity dedup)", async () => { + const { Timeline } = await import("./message-timeline.data") + const first = assistant("msg_activity_a", { activityID: "activity-A", revision: 0, state: "progress" }) + const second = assistant("msg_activity_b", { activityID: "activity-B", revision: 0, state: "progress" }) + const parts = new Map([ + [first.id, [reasoningPart(first.id, "prt_reasoning_A", INCIDENT_REASONING)]], + // Byte-identical reasoning text in a DIFFERENT activity. + [second.id, [reasoningPart(second.id, "prt_reasoning_B", INCIDENT_REASONING)]], + ]) + + const rows = Timeline.constructMessageRows( + user, + (id) => parts.get(id) ?? [], + [first, second], + 0, + true, + "idle", + false, + ) + const visible = partIDs(rows) + + // Both activities keep their reasoning — content-hash dedup across activities never happens. + expect(visible).toEqual(["prt_reasoning_A", "prt_reasoning_B"]) + }) + + test("28: legacy server with text-metadata markers only (no computed projection)", async () => { + const { Timeline } = await import("./message-timeline.data") + // Spec §7.2 (28): with Fix-E restored, legacy text-metadata markers (no computed + // `activityProgress` on messages) still converge per the old rules — the latest revision wins. + const first = assistant("msg_legacy_0") + const second = assistant("msg_legacy_1") + const parts = new Map([ + [ + first.id, + [ + legacyMarkerPart(first.id, "prt_legacy_marker_0", "revision 0", { + activity_id: "activity-legacy", + revision: 0, + state: "progress", + }), + ], + ], + [ + second.id, + [ + legacyMarkerPart(second.id, "prt_legacy_marker_1", "revision 1", { + activity_id: "activity-legacy", + revision: 1, + state: "progress", + }), + ], + ], + ]) + + const rows = Timeline.constructMessageRows( + user, + (id) => parts.get(id) ?? [], + [first, second], + 0, + false, + "idle", + false, + ) + const visible = partIDs(rows) + + // Legacy markers converge: the latest revision (1) is shown, the superseded revision (0) hidden. + expect(visible).toEqual(["prt_legacy_marker_1"]) + }) + + test("29: message projection vs legacy marker conflict — projection source wins; diagnostic status", async () => { + const { Timeline } = await import("./message-timeline.data") + // Spec §7.2 (29): with Fix-E restored, the message-owned projection wins over a conflicting + // legacy text marker, and a projection-conflict diagnostic is emitted (console.error). + const errorSpy = spyOn(console, "error") + try { + const message = assistant("msg_conflict", { + activityID: "activity-conflict", + revision: 9, // message-owned projection says revision 9 + state: "progress", + }) + const parts = new Map([ + [ + message.id, + [ + // Legacy marker disagrees: same activity but revision 8. + legacyMarkerPart(message.id, "prt_conflict_marker", "revision 8", { + activity_id: "activity-conflict", + revision: 8, + state: "progress", + }), + reasoningPart(message.id, "prt_conflict_reasoning", INCIDENT_REASONING), + ], + ], + ]) + + const rows = Timeline.constructMessageRows( + user, + (id) => parts.get(id) ?? [], + [message], + 0, + true, + "idle", + false, + ) + const visible = partIDs(rows) + + // The owning message's parts are rendered (projection-bearing message is not dropped). + expect(visible).toEqual(["prt_conflict_marker", "prt_conflict_reasoning"]) + // The projection-vs-legacy-marker conflict is diagnosed at the timeline layer. + expect(errorSpy).toHaveBeenCalled() + } finally { + errorSpy.mockRestore() + } + }) +}) diff --git a/packages/app/src/pages/session/message-timeline.data.test.ts b/packages/app/src/pages/session/message-timeline.data.test.ts index 2206a80b..a2a64d49 100644 --- a/packages/app/src/pages/session/message-timeline.data.test.ts +++ b/packages/app/src/pages/session/message-timeline.data.test.ts @@ -8,8 +8,7 @@ mock.module("@deepagent-code/ui/message-part", () => ({ type: "part", ref: { messageID: item.messageID, partID: item.part.id }, })), - renderable: (part: Part, showReasoningSummaries = true) => - part.type !== "reasoning" || showReasoningSummaries, + renderable: () => true, })) afterAll(() => mock.restore()) @@ -123,7 +122,7 @@ describe("message timeline activity progress", () => { }, }) as Part - test("renders every progress revision for one activity", async () => { + test("shows only the latest settled progress for one activity", async () => { const { Timeline } = await import("./message-timeline.data") const messages = [assistant("msg_a0"), assistant("msg_a1")] const parts = new Map([ @@ -134,10 +133,10 @@ describe("message timeline activity progress", () => { const rows = Timeline.constructMessageRows(user, (id) => parts.get(id) ?? [], messages, 0, false, "idle", false) expect( rows.flatMap((row) => (row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [])), - ).toEqual(["prt_progress_0", "prt_progress_1"]) + ).toEqual(["prt_progress_1"]) }) - test("keeps progress revisions when the activity final arrives", async () => { + test("replaces settled progress with the activity final", async () => { const { Timeline } = await import("./message-timeline.data") const messages = [assistant("msg_a0"), assistant("msg_a1"), { ...assistant("msg_a2"), finish: "stop" }] const parts = new Map([ @@ -149,10 +148,10 @@ describe("message timeline activity progress", () => { const rows = Timeline.constructMessageRows(user, (id) => parts.get(id) ?? [], messages, 0, false, "idle", false) expect( rows.flatMap((row) => (row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [])), - ).toEqual(["prt_progress_0", "prt_progress_1", "prt_final_2"]) + ).toEqual(["prt_final_2"]) }) - test("keeps text parts across separate parent user rows", async () => { + test("collapses every text part in one activity across separate parent user rows", async () => { const { Timeline } = await import("./message-timeline.data") const user2 = { ...user, id: "msg_user_2" } const firstAssistant = assistant("msg_cross_a0") @@ -174,6 +173,7 @@ describe("message timeline activity progress", () => { ], ]) const getParts = (id: string) => parts.get(id) ?? [] + const visibility = Timeline.activityProgressVisibility(messages, getParts) const firstRows = Timeline.constructMessageRows( user, getParts, @@ -182,6 +182,7 @@ describe("message timeline activity progress", () => { false, "idle", false, + visibility, ) const secondRows = Timeline.constructMessageRows( user2, @@ -191,12 +192,9 @@ describe("message timeline activity progress", () => { false, "idle", false, + visibility, ) - expect( - firstRows.flatMap((row) => - row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [], - ), - ).toEqual(["prt_progress_0", "prt_cross_old_plain"]) + expect(firstRows.some((row) => row._tag === "AssistantPart")).toBe(false) expect( secondRows.flatMap((row) => row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [], @@ -204,7 +202,7 @@ describe("message timeline activity progress", () => { ).toEqual(["prt_progress_1", "prt_cross_latest_plain"]) }) - test("keeps every text part in every revision", async () => { + test("applies one revision marker to every text part in the assistant message", async () => { const { Timeline } = await import("./message-timeline.data") const messages = [assistant("msg_multi_a0"), { ...assistant("msg_multi_a1"), finish: "stop" }] const plain = (messageID: string, id: string, text: string) => @@ -221,10 +219,10 @@ describe("message timeline activity progress", () => { expect( rows.flatMap((row) => (row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [])), - ).toEqual(["prt_progress_0", "prt_old_plain", "prt_final_1", "prt_final_plain"]) + ).toEqual(["prt_final_1", "prt_final_plain"]) }) - test("keeps reasoning summaries from every revision when enabled", async () => { + test("uses the message marker to collapse reasoning-only revisions", async () => { const { Timeline } = await import("./message-timeline.data") const messages = [ { @@ -247,34 +245,19 @@ describe("message timeline activity progress", () => { expect( rows.flatMap((row) => (row._tag === "AssistantPart" && row.group.type === "part" ? [row.group.ref.partID] : [])), - ).toEqual(["prt_reasoning_old", "prt_reasoning_latest"]) + ).toEqual(["prt_reasoning_latest"]) }) - test("uses showReasoning as the explicit reasoning visibility control", async () => { - const { Timeline } = await import("./message-timeline.data") - const message = assistant("msg_reasoning_setting") - const reasoning = { - id: "prt_reasoning_setting", - sessionID: user.sessionID, - messageID: message.id, - type: "reasoning", - text: "summary", - } as Part - const getParts = () => [reasoning] - - const hidden = Timeline.constructMessageRows(user, getParts, [message], 0, false, "idle", false) - const shown = Timeline.constructMessageRows(user, getParts, [message], 0, true, "idle", false) - - expect(hidden.some((row) => row._tag === "AssistantPart")).toBe(false) - expect(shown.some((row) => row._tag === "AssistantPart")).toBe(true) - }) - - test("keeps an older tool revision when a later terminal message has no renderable parts", async () => { + test("hides an older tool revision when the latest terminal message has no renderable parts", async () => { const { Timeline } = await import("./message-timeline.data") const old = { ...assistant("msg_tool_old"), activityProgress: { activityID: "activity-tool", revision: 0, state: "progress" as const }, } + const terminal = { + ...assistant("msg_tool_terminal"), + activityProgress: { activityID: "activity-tool", revision: 1, state: "final" as const }, + } const tool = { id: "prt_tool_old", sessionID: user.sessionID, @@ -285,8 +268,10 @@ describe("message timeline activity progress", () => { state: { status: "completed", input: {}, output: "pending", title: "poll", time: { start: 1, end: 2 } }, } as Part const parts = new Map([[old.id, [tool]]]) - const rows = Timeline.constructMessageRows(user, (id) => parts.get(id) ?? [], [old], 0, true, "idle", false) + const visibility = Timeline.activityProgressVisibility([old, terminal], (id) => parts.get(id) ?? []) + + const rows = Timeline.constructMessageRows(user, (id) => parts.get(id) ?? [], [old], 0, true, "idle", false, visibility) - expect(rows.some((row) => row._tag === "AssistantPart")).toBe(true) + expect(rows.some((row) => row._tag === "AssistantPart")).toBe(false) }) }) diff --git a/packages/app/src/pages/session/message-timeline.data.ts b/packages/app/src/pages/session/message-timeline.data.ts index cf4b98d9..ea793845 100644 --- a/packages/app/src/pages/session/message-timeline.data.ts +++ b/packages/app/src/pages/session/message-timeline.data.ts @@ -147,6 +147,7 @@ export namespace Timeline { showReasoning: boolean, status: SessionStatus["type"], isActive: boolean, + activityProgressVisibility?: ReadonlySet, ) { const rows: TimelineRow.TimelineRow[] = [] @@ -158,10 +159,14 @@ export namespace Timeline { const interrupted = interruptedMessageIndex !== -1 const error = assistantMessages.find((m) => m.error && m.error.name !== "MessageAbortedError")?.error - const assistantPartRefs = assistantMessages.flatMap((message, messageIndex) => - getMessageParts(message.id) - .filter((part) => renderable(part, showReasoning)) - .map((part) => ({ messageID: message.id, messageIndex, part })), + const assistantPartRefs = latestActivityProgress( + assistantMessages, + assistantMessages.flatMap((message, messageIndex) => + getMessageParts(message.id) + .filter((part) => renderable(part, showReasoning)) + .map((part) => ({ messageID: message.id, messageIndex, part })), + ), + activityProgressVisibility, ) const assistantItems = interrupted && !compaction @@ -286,6 +291,100 @@ export namespace Timeline { return rows } + // BUG-407-008 Fix-E (restored after 1ae96499 removed it): per-Activity revision convergence. For + // each activityID, only the LATEST revision's parts are rendered (a terminal state beats a + // non-terminal one; among equal terminality the higher revision wins). Superseded older revisions + // are hidden, so identical reasoning re-emitted across revisions is shown once. + export function activityProgressVisibility( + assistantMessages: AssistantMessage[], + getMessageParts: (messageID: string) => Part[], + ) { + const refs = assistantMessages.flatMap((message, messageIndex) => + getMessageParts(message.id).map((part) => ({ messageID: message.id, messageIndex, part })), + ) + return new Set(latestActivityProgress(assistantMessages, refs).map((ref) => `${ref.messageID}:${ref.part.id}`)) + } + + function latestActivityProgress( + assistantMessages: AssistantMessage[], + refs: T[], + visibility?: ReadonlySet, + ) { + const progressByMessage = new Map>>() + assistantMessages.forEach((message) => { + const marker = messageActivityProgress(message) + if (marker) progressByMessage.set(message.id, marker) + }) + refs.forEach((ref) => { + const marker = partActivityProgress(ref.part) + const projected = progressByMessage.get(ref.messageID) + if ( + marker && + projected && + (marker.activityID !== projected.activityID || marker.revision !== projected.revision) + ) + console.error("Conflicting activity progress projection", { + messageID: ref.messageID, + projected, + legacy: marker, + }) + if (projected) return + if (marker) progressByMessage.set(ref.messageID, marker) + }) + const markerFor = (ref: T) => progressByMessage.get(ref.messageID) ?? partActivityProgress(ref.part) + if (visibility) + return refs.filter((ref) => { + if (!markerFor(ref)) return true + return visibility.has(`${ref.messageID}:${ref.part.id}`) + }) + const selected = new Map() + progressByMessage.forEach((marker) => { + if (!marker) return + const terminal = marker.state !== "provisional" && marker.state !== "progress" + const current = selected.get(marker.activityID) + if ( + current && + ((current.terminal && !terminal) || (current.terminal === terminal && current.revision > marker.revision)) + ) + return + selected.set(marker.activityID, { revision: marker.revision, terminal }) + }) + return refs.filter((ref) => { + const marker = markerFor(ref) + if (!marker) return true + const current = selected.get(marker.activityID) + return ( + current?.revision === marker.revision && + current.terminal === (marker.state !== "provisional" && marker.state !== "progress") + ) + }) + } + + function messageActivityProgress(message: AssistantMessage) { + const marker = message.activityProgress + if (!marker) return + if (!marker.activityID || !Number.isInteger(marker.revision) || marker.revision < 0) return + if (!["provisional", "progress", "final", "interrupted", "recovery_required", "failed"].includes(marker.state)) + return + return marker + } + + function partActivityProgress(part: Part) { + if (part.type !== "text") return + const value = part.metadata?.deepagent_activity_progress + if (!value || typeof value !== "object") return + const marker = value as Record + if (typeof marker.activity_id !== "string" || marker.activity_id.length === 0) return + if (typeof marker.revision !== "number" || !Number.isInteger(marker.revision) || marker.revision < 0) return + if (!["provisional", "progress", "final", "interrupted", "recovery_required"].includes(String(marker.state))) + return + return { + activityID: marker.activity_id, + revision: marker.revision, + state: marker.state as "provisional" | "progress" | "final" | "interrupted" | "recovery_required", + } + } + function isSummaryDiff(value: SnapshotFileDiff): value is SummaryDiff { return typeof value.file === "string" } diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index b3ee9d86..05018db1 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -89,6 +89,58 @@ const emptyTools: ToolPart[] = [] const emptyAssistantMessages: AssistantMessage[] = [] const idle = { type: "idle" as const } +// --------------------------------------------------------------------------- +// UX-001: only virtualize large, completed tool diffs. +// --------------------------------------------------------------------------- + +// Threshold semantics mirror VIRTUALIZE_BYTES (500KB) in packages/ui/src/components/file.tsx. +const TOOL_DIFF_VIRTUALIZE_BYTES = 500_000 + +function payloadStringLength(value: unknown) { + return typeof value === "string" ? value.length : 0 +} + +// Tool parts carry no dedicated "diff size" field and we intentionally avoid adding +// schema/persistence for this, so the size is estimated conservatively from existing +// string payloads on the completed part: +// 1. state.metadata.filediff before/after/patch lengths (edit tool diff source) +// 2. state.metadata.files[].patch/diff/before/after lengths (apply_patch diff source) +// 3. fallback: state.output length as a coarse proxy (only universally available signal) +export function toolDiffPayloadSize(part: PartType): number { + if (part.type !== "tool") return 0 + const state = part.state + // Streaming (pending/running) and error parts have no stable size; virtualizing them + // would force repeated viewer resets as content grows (see file.tsx render reset logic). + if (state.status !== "completed") return 0 + const metadata: Record = state.metadata + let size = 0 + const filediff = metadata.filediff + if (filediff && typeof filediff === "object") { + const fd = filediff as Record + size = Math.max(size, payloadStringLength(fd.patch), payloadStringLength(fd.before), payloadStringLength(fd.after)) + } + const files = metadata.files + if (Array.isArray(files)) { + for (const item of files) { + if (!item || typeof item !== "object") continue + const file = item as Record + size = Math.max( + size, + payloadStringLength(file.patch), + payloadStringLength(file.diff), + payloadStringLength(file.before), + payloadStringLength(file.after), + ) + } + } + if (size === 0) size = payloadStringLength(state.output) + return size +} + +export function shouldVirtualizeToolDiff(part: PartType): boolean { + return toolDiffPayloadSize(part) > TOOL_DIFF_VIRTUALIZE_BYTES +} + // ForkBanner and BottomSpacer render their own top-level container (no TimelineRowFrame, which // assumes a per-turn `userMessageID`), so they're excluded from the framed-row union. type FramedTimelineRow = Exclude @@ -436,6 +488,10 @@ function TimelineDiffView(props: { diff: SummaryDiff }) { return (
+ {/* UX-001: summary diffs stay non-virtualized. SummaryDiff only carries patch/addition + counts (no reliable size signal), the wrapper has overflow-y:auto but no bounded + height, so an inner virtualizer would still share the outer row-level scroll root. + Keeping virtualize={false} conservatively. */}
) @@ -586,6 +642,12 @@ export function MessageTimeline(props: { }) const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new")) const getMsgParts = (msgId: string) => sync.data.part[msgId] ?? emptyParts + const activityProgressVisibility = createMemo(() => + Timeline.activityProgressVisibility( + sessionMessages().filter((message): message is AssistantMessage => message.role === "assistant"), + getMsgParts, + ), + ) const childTaskDescription = createMemo(() => { const id = sessionID() if (!id) return @@ -616,6 +678,7 @@ export function MessageTimeline(props: { settings.general.showReasoningSummaries(), sessionStatus().type, activeMessageID() === userMessage.id, + activityProgressVisibility(), ) return reuseTimelineRows(previous, rows) @@ -1050,6 +1113,31 @@ export function MessageTimeline(props: { navigate(`/${params.dir}/session`) } + const exportSession = async (sessionID: string) => { + const session = sync.session.get(sessionID) + const base = (session?.title || sessionID).replace(/[^\w\u4e00-\u9fff-]+/g, "-").slice(0, 60) || sessionID + try { + const res = await sdk.client.session.exportSnapshot({ sessionID }, { throwOnError: true }) + const bundle = typeof res.data === "string" ? res.data : JSON.stringify(res.data ?? {}) + const url = URL.createObjectURL(new Blob([bundle], { type: "application/json" })) + const anchor = document.createElement("a") + anchor.href = url + anchor.download = `${base}-snapshot.json` + document.body.appendChild(anchor) + anchor.click() + anchor.remove() + URL.revokeObjectURL(url) + showToast({ + variant: "success", + icon: "circle-check", + title: language.t("session.export.success"), + }) + } catch (err) { + console.error("Failed to export session snapshot", err) + showToast({ title: language.t("common.requestFailed") }) + } + } + const archiveSession = async (sessionID: string) => { const session = sync.session.get(sessionID) if (!session) return @@ -1304,7 +1392,8 @@ export function MessageTimeline(props: { toolOpen={toolOpen[part().id] ?? defaultOpen()} onToolOpenChange={(open) => setToolOpen(part().id, open)} deferToolContent={false} - virtualizeDiff={false} + // UX-001: only completed + large tool diffs get virtualized; streaming parts stay false. + virtualizeDiff={shouldVirtualizeToolDiff(part())} onFork={props.actions?.fork} /> )} @@ -1816,6 +1905,9 @@ export function MessageTimeline(props: { + void exportSession(id)}> + {language.t("session.export.action")} + void archiveSession(id)}> {language.t("common.archive")} diff --git a/packages/app/src/pages/session/session-side-panel.tsx b/packages/app/src/pages/session/session-side-panel.tsx index 9a3e7695..2332de65 100644 --- a/packages/app/src/pages/session/session-side-panel.tsx +++ b/packages/app/src/pages/session/session-side-panel.tsx @@ -43,6 +43,7 @@ import { SidePanelDebug } from "@/pages/session/side-panel-debug" import { SidePanelProfile } from "@/pages/session/side-panel-profile" import { SidePanelIM } from "@/pages/session/side-panel-im" import { SidePanelContext } from "@/pages/session/side-panel-context" +import { SidePanelStats } from "@/pages/session/side-panel-stats" import { SidePanelDockHeader, SidePanelTerminal, SidePanelDebugConsole } from "@/pages/session/side-panel-terminal" import { ProblemsPanel } from "@/pages/session/problems-panel" import { PanelErrorBoundary } from "@/pages/session/panel-error-boundary" @@ -65,6 +66,7 @@ type PanelMode = | "plugins" | "debug" | "profile" + | "stats" | "terminal" | "debug-console" | "problems" @@ -117,6 +119,8 @@ const PANELS: readonly PanelDef[] = [ { mode: "plugins", icon: "plugin", titleKey: "status.popover.tab.plugins", group: "dev", bucket: "narrow" }, { mode: "debug", icon: "debug", titleKey: "session.panel.debug", group: "dev", bucket: "narrow" }, { mode: "profile", icon: "profile", titleKey: "session.panel.profile", group: "dev", bucket: "narrow" }, + // PARITY-001: cross-session cost/token overview aggregated from the synced session store. + { mode: "stats", icon: "dash", titleKey: "session.stats.title", group: "dev", bucket: "narrow" }, // Dock — the movable panels; only surface here when docked to the side (see gating below). { mode: "terminal", @@ -693,6 +697,9 @@ export function SessionSidePanel(props: { + + + diff --git a/packages/app/src/pages/session/side-panel-stats.tsx b/packages/app/src/pages/session/side-panel-stats.tsx new file mode 100644 index 00000000..63ac6ff9 --- /dev/null +++ b/packages/app/src/pages/session/side-panel-stats.tsx @@ -0,0 +1,129 @@ +// PARITY-001: GUI cross-session cost/token overview. +// +// The CLI `stats` command aggregates the whole database in-process; the GUI only had per-session +// context diagnostics (side-panel-context.tsx). This panel aggregates the sessions already synced +// into the directory store (sync.data.session — the same list the session drawer renders) into a +// per-session cost/token/model overview. Zero new API endpoints: the SDK Session type carries +// `cost`, `tokens` and `model` rollups. +import { Icon } from "@deepagent-code/ui/icon" +import { IconButton } from "@deepagent-code/ui/icon-button" +import { For, Show, createMemo, type Component, type JSX } from "solid-js" +import { useLanguage } from "@/context/language" +import { useSync } from "@/context/sync" +import { + aggregateSessionStats, + formatCost, + formatTokenCount, + sortModelUsage, +} from "@/utils/session-stats" + +export const SidePanelStats: Component<{ onClose: () => void }> = (props) => { + const sync = useSync() + const language = useLanguage() + + // The synced session list for the current directory; events keep it live, so the memo + // re-aggregates automatically as session cost/tokens rollups update. + const stats = createMemo(() => aggregateSessionStats(sync.data.session)) + const models = createMemo(() => sortModelUsage(stats().modelUsage)) + + const Row = (row: { label: string; value: string }) => ( +
+ {row.label} + {row.value} +
+ ) + + const Section = (section: { title: string; children?: JSX.Element }) => ( +
+
{section.title}
+
{section.children}
+
+ ) + + return ( +
+
+ +
+ {language.t("session.stats.title")} +
+ +
+ +
+ 0} + fallback={
{language.t("session.stats.noData")}
} + > +
+ + +
+ +
+ + + + + + + + + +
+ + 0}> +
+ + {([model, usage]) => ( +
+
+ + {model} + + + {formatCost(usage.cost, 4)} + +
+
+ + {language.t("session.stats.sessions")}: {usage.sessions.toLocaleString()} + + + {formatTokenCount(usage.tokens.input)}↑ {formatTokenCount(usage.tokens.output)}↓ + + + {language.t("session.stats.cacheRead")} {formatTokenCount(usage.tokens.cache.read)} + +
+
+ )} +
+
+
+
+
+
+ ) +} diff --git a/packages/app/src/utils/message-order.test.ts b/packages/app/src/utils/message-order.test.ts new file mode 100644 index 00000000..85927655 --- /dev/null +++ b/packages/app/src/utils/message-order.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, test } from "bun:test" +import type { Message, Part } from "@deepagent-code/sdk/v2/client" +import { + compareMessages, + compareParts, + findMessageIndex, + locateMessage, + locatePart, + messageOrderKey, + messageTime, + partOrderKey, + partTime, +} from "./message-order" + +// Regression fixture from BUG-407-012: the 26th wrap of the 6-byte ID time field happened on +// 2026-08-14, so the chronologically NEWER message carries a lexicographically SMALLER ID. +const WRAP_OLD_ID = "msg_ffa88f0840015Xj7vIrcdNEJJB" // 2026-08-13 17:51:46, before the wrap +const WRAP_NEW_ID = "msg_00d62a3c4001KqYw3o8wBBH6qm" // 2026-08-17 09:42:43, after the wrap +const WRAP_OLD_TIME = 1786614706000 +const WRAP_NEW_TIME = 1786930963000 + +const userMessage = (id: string, created: number) => + ({ + id, + sessionID: "ses_1", + role: "user", + time: { created }, + agent: "assistant", + model: { providerID: "openai", modelID: "gpt" }, + }) as Message + +const textPart = (id: string, start?: number) => + ({ + id, + sessionID: "ses_1", + messageID: "msg_1", + type: "text", + text: id, + ...(start === undefined ? {} : { time: { start } }), + }) as Part + +describe("message order (BUG-407-012 root cause B)", () => { + test("orders messages by time.created first, then by ID", () => { + const older = userMessage(WRAP_OLD_ID, WRAP_OLD_TIME) + const newer = userMessage(WRAP_NEW_ID, WRAP_NEW_TIME) + expect(WRAP_NEW_ID < WRAP_OLD_ID).toBe(true) // sanity: raw ID order is reversed + expect(compareMessages(newer, older)).toBeGreaterThan(0) + expect(compareMessages(older, newer)).toBeLessThan(0) + expect(messageOrderKey(older) < messageOrderKey(newer)).toBe(true) + expect([newer, older].sort((a, b) => compareMessages(a, b)).map((m) => m.id)).toEqual([ + WRAP_OLD_ID, + WRAP_NEW_ID, + ]) + }) + + test("falls back to ID order when time.created is equal or missing", () => { + const a = userMessage("msg_a", 5) + const b = userMessage("msg_b", 5) + expect(compareMessages(a, b)).toBeLessThan(0) + expect(messageTime({ id: "msg_x" })).toBe(0) + expect(compareMessages({ id: "msg_x" }, a)).toBeLessThan(0) // missing time sorts earliest + }) + + test("locates a wrapped newer message at the tail, never at the head", () => { + const sorted = [userMessage(WRAP_OLD_ID, WRAP_OLD_TIME)] + const insert = locateMessage(sorted, userMessage(WRAP_NEW_ID, WRAP_NEW_TIME)) + expect(insert).toEqual({ found: false, index: 1 }) + const existing = locateMessage(sorted, userMessage(WRAP_OLD_ID, WRAP_OLD_TIME)) + expect(existing).toEqual({ found: true, index: 0 }) + expect(findMessageIndex(sorted, WRAP_OLD_ID)).toBe(0) + }) + + test("locateMessage finds an item by identity even when its time differs", () => { + const sorted = [userMessage(WRAP_OLD_ID, WRAP_OLD_TIME)] + const drifted = locateMessage(sorted, { id: WRAP_OLD_ID, time: { created: 1 } }) + expect(drifted).toEqual({ found: true, index: 0 }) + }) +}) + +describe("part order (BUG-407-012 root cause B)", () => { + test("orders parts by time.start first, then by ID", () => { + const older = textPart("prt_ffa88f084001aaaa", WRAP_OLD_TIME) + const newer = textPart("prt_00d62a3c4001bbbb", WRAP_NEW_TIME) + expect(newer.id < older.id).toBe(true) // sanity: raw ID order is reversed + expect(compareParts(newer, older)).toBeGreaterThan(0) + expect(partOrderKey(older) < partOrderKey(newer)).toBe(true) + expect([newer, older].sort((a, b) => compareParts(a, b)).map((p) => p.id)).toEqual([older.id, newer.id]) + }) + + test("parts without time.start fall back to 0 and locate by identity", () => { + const timeless = textPart("prt_tool") + expect(partTime(timeless)).toBe(0) + const timed = textPart("prt_aaaa", WRAP_OLD_TIME) + expect(compareParts(timeless, timed)).toBeLessThan(0) + const located = locatePart([timeless], { ...timed, id: "prt_tool" }) + expect(located.found).toBe(true) + }) +}) diff --git a/packages/app/src/utils/message-order.ts b/packages/app/src/utils/message-order.ts new file mode 100644 index 00000000..64af1145 --- /dev/null +++ b/packages/app/src/utils/message-order.ts @@ -0,0 +1,87 @@ +import { Binary } from "@deepagent-code/core/util/binary" + +// BUG-407-012 root cause B: opaque IDs encode a 36-bit millisecond timestamp that wraps every +// 2^36 ms (~795 days; the 26th wrap happened on 2026-08-14). After a wrap, newly generated +// `msg_00...` IDs sort lexicographically BEFORE older `msg_ff...` IDs, so ID order is no longer +// chronological order. IDs must be treated as pure identity; every timeline sort / binary +// location / "latest" decision uses the compound (time, id) order defined here instead. +// +// - messages order by `time.created` (missing -> 0) +// - parts order by `time.start` (missing -> 0) +// - the tie-break key remains the existing lexicographic ID comparison + +export type MessageOrderItem = { id: string; time?: { created?: number } } +// Part `time` shapes differ across variants (`{start}` on text/reasoning, `{created}` on retry, +// absent on tools), so the order item accepts any time shape and reads `start` defensively. +export type PartOrderItem = { id: string; time?: Record } + +// Millisecond epochs fit in 13 digits today; 16 keeps the fixed-width prefix sortable for eons. +const TIME_FIELD_WIDTH = 16 + +export const compareIDs = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0) + +const normalizeTime = (value: number | undefined) => + typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0 + +export const messageTime = (message: MessageOrderItem) => normalizeTime(message.time?.created) + +export const partTime = (part: PartOrderItem) => normalizeTime(part.time?.start as number | undefined) + +const timePrefix = (time: number) => String(time).padStart(TIME_FIELD_WIDTH, "0") + +// Lexicographic comparison of these keys is exactly the compound (time, id) order, so they can be +// used with key-based Binary.search and with plain string `<`/`>` boundary checks (e.g. revert). +export const messageOrderKey = (message: MessageOrderItem) => timePrefix(messageTime(message)) + message.id + +export const partOrderKey = (part: PartOrderItem) => timePrefix(partTime(part)) + part.id + +export const compareMessages = (a: MessageOrderItem, b: MessageOrderItem) => { + const delta = messageTime(a) - messageTime(b) + if (delta !== 0) return delta < 0 ? -1 : 1 + return compareIDs(a.id, b.id) +} + +export const compareParts = (a: PartOrderItem, b: PartOrderItem) => { + const delta = partTime(a) - partTime(b) + if (delta !== 0) return delta < 0 ? -1 : 1 + return compareIDs(a.id, b.id) +} + +export const searchMessages = (messages: readonly T[], message: MessageOrderItem) => + Binary.search(messages as unknown as MessageOrderItem[], messageOrderKey(message), (item) => messageOrderKey(item)) + +export const searchParts = (parts: readonly T[], part: PartOrderItem) => + Binary.search(parts as unknown as PartOrderItem[], partOrderKey(part), (item) => partOrderKey(item)) + +// A bare ID carries no time, so it cannot be binary-searched in a (time, id)-ordered array. +// Linear scans are bounded: one session's loaded messages / one message's parts. +export const findMessageIndex = (messages: readonly T[], id: string) => + messages.findIndex((item) => item.id === id) + +export const findPartIndex = (parts: readonly T[], id: string) => + parts.findIndex((item) => item.id === id) + +// Locate an item by identity without ever duplicating it: prefer the compound-order binary search +// (fast path; also yields the correct insertion index when absent), then fall back to an ID scan +// for the rare case where the stored copy carries a different time field than the incoming one. +export function locateMessage( + messages: readonly T[], + message: MessageOrderItem, +): { found: boolean; index: number } { + const result = searchMessages(messages, message) + if (result.found) return result + const index = findMessageIndex(messages, message.id) + if (index !== -1) return { found: true, index } + return result +} + +export function locatePart(parts: readonly T[], part: PartOrderItem): { + found: boolean + index: number +} { + const result = searchParts(parts, part) + if (result.found) return result + const index = findPartIndex(parts, part.id) + if (index !== -1) return { found: true, index } + return result +} diff --git a/packages/app/src/utils/session-stats.test.ts b/packages/app/src/utils/session-stats.test.ts new file mode 100644 index 00000000..d238d22f --- /dev/null +++ b/packages/app/src/utils/session-stats.test.ts @@ -0,0 +1,181 @@ +import { describe, expect, test } from "bun:test" +import { + aggregateSessionStats, + formatCost, + formatTokenCount, + sortModelUsage, + type StatsSession, +} from "./session-stats" + +const MS_IN_DAY = 24 * 60 * 60 * 1000 + +function session(overrides: Partial & { id: string }): StatsSession { + return { + time: { created: 0, updated: 0 }, + ...overrides, + } +} + +describe("aggregateSessionStats", () => { + test("empty input yields zeroed stats without NaN", () => { + const stats = aggregateSessionStats([]) + expect(stats.totalSessions).toBe(0) + expect(stats.totalCost).toBe(0) + expect(stats.totalTokens.input).toBe(0) + expect(stats.tokensPerSession).toBe(0) + expect(stats.medianTokensPerSession).toBe(0) + expect(stats.costPerDay).toBe(0) + expect(Object.keys(stats.modelUsage)).toHaveLength(0) + }) + + test("sums cost and tokens across sessions", () => { + const stats = aggregateSessionStats([ + session({ + id: "a", + cost: 1.5, + tokens: { input: 100, output: 50, reasoning: 10, cache: { read: 5, write: 2 } }, + }), + session({ + id: "b", + cost: 0.25, + tokens: { input: 200, output: 30, reasoning: 0, cache: { read: 15, write: 8 } }, + }), + ]) + expect(stats.totalSessions).toBe(2) + expect(stats.totalCost).toBeCloseTo(1.75) + expect(stats.totalTokens.input).toBe(300) + expect(stats.totalTokens.output).toBe(80) + expect(stats.totalTokens.reasoning).toBe(10) + expect(stats.totalTokens.cache.read).toBe(20) + expect(stats.totalTokens.cache.write).toBe(10) + }) + + test("groups model usage by provider/model key", () => { + const stats = aggregateSessionStats([ + session({ + id: "a", + cost: 1, + tokens: { input: 10, output: 5, reasoning: 1, cache: { read: 2, write: 3 } }, + model: { providerID: "anthropic", id: "claude" }, + }), + session({ + id: "b", + cost: 2, + tokens: { input: 20, output: 10, reasoning: 0, cache: { read: 0, write: 0 } }, + model: { providerID: "anthropic", id: "claude" }, + }), + session({ + id: "c", + cost: 4, + tokens: { input: 1, output: 1, reasoning: 0, cache: { read: 0, write: 0 } }, + model: { providerID: "openai", id: "gpt" }, + }), + ]) + const claude = stats.modelUsage["anthropic/claude"] + const gpt = stats.modelUsage["openai/gpt"] + expect(claude).toBeDefined() + expect(claude.sessions).toBe(2) + expect(claude.cost).toBeCloseTo(3) + expect(claude.tokens.input).toBe(30) + expect(claude.tokens.output).toBe(15) + expect(claude.tokens.reasoning).toBe(1) + expect(claude.tokens.cache.read).toBe(2) + expect(claude.tokens.cache.write).toBe(3) + expect(gpt).toBeDefined() + expect(gpt.sessions).toBe(1) + expect(gpt.cost).toBeCloseTo(4) + }) + + test("sessions without rollups contribute zero and stay out of model usage", () => { + const stats = aggregateSessionStats([ + session({ id: "a" }), + session({ id: "b", cost: Number.NaN, tokens: undefined }), + session({ + id: "c", + cost: 1, + tokens: { input: 4, output: 6, reasoning: 0, cache: { read: 0, write: 0 } }, + model: { providerID: "p", id: "m" }, + }), + ]) + expect(stats.totalSessions).toBe(3) + expect(stats.totalCost).toBeCloseTo(1) + expect(stats.totalTokens.input).toBe(4) + expect(stats.totalTokens.output).toBe(6) + expect(Object.keys(stats.modelUsage)).toEqual(["p/m"]) + }) + + test("computes per-session averages and median (even count)", () => { + const stats = aggregateSessionStats([ + session({ id: "a", tokens: { input: 10, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } }), + session({ id: "b", tokens: { input: 30, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } }), + session({ id: "c", tokens: { input: 100, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } }), + session({ id: "d", tokens: { input: 200, output: 0, reasoning: 0, cache: { read: 0, write: 0 } } }), + ]) + expect(stats.tokensPerSession).toBeCloseTo((10 + 30 + 100 + 200) / 4) + expect(stats.medianTokensPerSession).toBeCloseTo((30 + 100) / 2) + }) + + test("computes date range, days and cost per day", () => { + const start = Date.UTC(2026, 0, 1) + const stats = aggregateSessionStats([ + session({ id: "a", cost: 3, time: { created: start, updated: start + MS_IN_DAY } }), + session({ id: "b", cost: 5, time: { created: start + MS_IN_DAY, updated: start + 3 * MS_IN_DAY } }), + ]) + expect(stats.dateRange.earliest).toBe(start) + expect(stats.dateRange.latest).toBe(start + 3 * MS_IN_DAY) + expect(stats.days).toBe(3) + expect(stats.costPerDay).toBeCloseTo(8 / 3) + }) + + test("single session collapses to one day", () => { + const at = Date.UTC(2026, 0, 1) + const stats = aggregateSessionStats([session({ id: "a", cost: 2, time: { created: at, updated: at } })]) + expect(stats.days).toBe(1) + expect(stats.costPerDay).toBeCloseTo(2) + }) +}) + +describe("formatTokenCount", () => { + test("keeps small numbers verbatim", () => { + expect(formatTokenCount(0)).toBe("0") + expect(formatTokenCount(999)).toBe("999") + }) + + test("compacts thousands and millions", () => { + expect(formatTokenCount(1_500)).toBe("1.5K") + expect(formatTokenCount(2_500_000)).toBe("2.5M") + }) + + test("tolerates non-finite input", () => { + expect(formatTokenCount(Number.NaN)).toBe("0") + expect(formatTokenCount(Number.POSITIVE_INFINITY)).toBe("0") + }) +}) + +describe("formatCost", () => { + test("formats dollars with two decimals by default", () => { + expect(formatCost(1.2345)).toBe("$1.23") + expect(formatCost(0)).toBe("$0.00") + }) + + test("supports higher precision for model rows", () => { + expect(formatCost(0.1234, 4)).toBe("$0.1234") + }) + + test("tolerates non-finite input", () => { + expect(formatCost(Number.NaN)).toBe("$0.00") + }) +}) + +describe("sortModelUsage", () => { + test("sorts by cost descending with session count tiebreak", () => { + const usage = aggregateSessionStats([ + session({ id: "a", cost: 1, model: { providerID: "p", id: "cheap" } }), + session({ id: "b", cost: 9, model: { providerID: "p", id: "pricey" } }), + session({ id: "c", cost: 1, model: { providerID: "p", id: "busy" } }), + session({ id: "d", cost: 0, model: { providerID: "p", id: "busy" } }), + ]).modelUsage + const sorted = sortModelUsage(usage).map(([key]) => key) + expect(sorted).toEqual(["p/pricey", "p/busy", "p/cheap"]) + }) +}) diff --git a/packages/app/src/utils/session-stats.ts b/packages/app/src/utils/session-stats.ts new file mode 100644 index 00000000..db28121d --- /dev/null +++ b/packages/app/src/utils/session-stats.ts @@ -0,0 +1,188 @@ +// PARITY-001: cross-session cost/token aggregation for the GUI. +// +// Mirrors the CLI `stats` command (packages/deepagent-code/src/cli/cmd/stats.ts) but aggregates +// over the sessions already synced into the directory store (`store.session`) instead of reading +// the database directly. The SDK `Session` type carries `cost`, `tokens` and `model` per session, +// so no new API endpoint is needed. Per-message breakdowns (tool usage, per-message model usage) +// are intentionally out of scope here — the store only carries session-level rollups. + +/** Structural slice of the SDK `Session` shape this aggregation consumes. */ +export interface StatsSession { + readonly id: string + readonly parentID?: string + readonly cost?: number + readonly tokens?: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { + readonly read: number + readonly write: number + } + } + readonly model?: { + readonly id: string + readonly providerID: string + } + readonly time: { + readonly created: number + readonly updated: number + } +} + +export interface StatsModelUsage { + readonly sessions: number + readonly cost: number + readonly tokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { + readonly read: number + readonly write: number + } + } +} + +export interface SessionStats { + readonly totalSessions: number + readonly totalCost: number + readonly totalTokens: { + readonly input: number + readonly output: number + readonly reasoning: number + readonly cache: { + readonly read: number + readonly write: number + } + } + /** Keyed by `${providerID}/${modelID}`, matching the CLI stats model keys. */ + readonly modelUsage: Record + readonly dateRange: { + readonly earliest: number + readonly latest: number + } + readonly days: number + readonly costPerDay: number + readonly tokensPerSession: number + readonly medianTokensPerSession: number +} + +const MS_IN_DAY = 24 * 60 * 60 * 1000 + +const emptyTokens = () => ({ input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }) + +const totalOf = (tokens: SessionStats["totalTokens"]) => + tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write + +/** + * Aggregate per-session cost/token rollups into a cross-session overview. + * + * Pure and allocation-light so it can run inside a `createMemo` on every store update. Sessions + * missing `cost`/`tokens` (older rows, not yet rolled up) contribute zero. + */ +export function aggregateSessionStats(sessions: readonly StatsSession[]): SessionStats { + const totalTokens = emptyTokens() + const modelUsage: Record = {} + const sessionTotals: number[] = [] + let totalCost = 0 + let earliest = 0 + let latest = 0 + + for (const session of sessions) { + const cost = typeof session.cost === "number" && Number.isFinite(session.cost) ? session.cost : 0 + const tokens = session.tokens + totalCost += cost + if (tokens) { + totalTokens.input += tokens.input || 0 + totalTokens.output += tokens.output || 0 + totalTokens.reasoning += tokens.reasoning || 0 + totalTokens.cache.read += tokens.cache?.read || 0 + totalTokens.cache.write += tokens.cache?.write || 0 + } + sessionTotals.push(totalOf({ ...emptyTokens(), ...tokens, cache: { ...emptyTokens().cache, ...tokens?.cache } })) + + if (session.model) { + const key = `${session.model.providerID}/${session.model.id}` + const existing = modelUsage[key] + const merged: StatsModelUsage = existing + ? { + sessions: existing.sessions + 1, + cost: existing.cost + cost, + tokens: { + input: existing.tokens.input + (tokens?.input || 0), + output: existing.tokens.output + (tokens?.output || 0), + reasoning: existing.tokens.reasoning + (tokens?.reasoning || 0), + cache: { + read: existing.tokens.cache.read + (tokens?.cache?.read || 0), + write: existing.tokens.cache.write + (tokens?.cache?.write || 0), + }, + }, + } + : { + sessions: 1, + cost, + tokens: { + input: tokens?.input || 0, + output: tokens?.output || 0, + reasoning: tokens?.reasoning || 0, + cache: { + read: tokens?.cache?.read || 0, + write: tokens?.cache?.write || 0, + }, + }, + } + modelUsage[key] = merged + } + + const created = session.time?.created ?? 0 + const updated = session.time?.updated ?? created + if (earliest === 0 || (created > 0 && created < earliest)) earliest = created + if (updated > latest) latest = updated + } + + const days = earliest > 0 && latest > earliest ? Math.max(1, Math.ceil((latest - earliest) / MS_IN_DAY)) : 1 + const grandTotal = totalOf(totalTokens) + sessionTotals.sort((a, b) => a - b) + const mid = Math.floor(sessionTotals.length / 2) + const median = + sessionTotals.length === 0 + ? 0 + : sessionTotals.length % 2 === 0 + ? (sessionTotals[mid - 1] + sessionTotals[mid]) / 2 + : sessionTotals[mid] + + return { + totalSessions: sessions.length, + totalCost, + totalTokens, + modelUsage, + dateRange: { earliest, latest }, + days, + costPerDay: totalCost / days, + tokensPerSession: sessions.length > 0 ? grandTotal / sessions.length : 0, + medianTokensPerSession: median, + } +} + +/** Compact token formatting shared with the CLI display (1.2K / 3.4M). */ +export function formatTokenCount(num: number): string { + if (!Number.isFinite(num)) return "0" + const value = Math.round(num) + if (value >= 1_000_000) return (value / 1_000_000).toFixed(1) + "M" + if (value >= 1_000) return (value / 1_000).toFixed(1) + "K" + return value.toString() +} + +/** Dollar formatting for overview-level costs (two decimals). */ +export function formatCost(cost: number, decimals = 2): string { + const value = Number.isFinite(cost) ? cost : 0 + return `$${value.toFixed(decimals)}` +} + +/** Model usage rows sorted by cost descending (ties: session count descending). */ +export function sortModelUsage(modelUsage: Record): Array<[string, StatsModelUsage]> { + return Object.entries(modelUsage).sort( + ([, a], [, b]) => b.cost - a.cost || b.sessions - a.sessions, + ) +} diff --git a/packages/cli/script/generate.ts b/packages/cli/script/generate.ts index 9ad25383..b2977434 100755 --- a/packages/cli/script/generate.ts +++ b/packages/cli/script/generate.ts @@ -1,7 +1,23 @@ -const modelsUrl = process.env.DEEPAGENT_CODE_MODELS_URL || "https://models.dev" +// Clean-build policy: ordered list of fresh remote endpoints; fail closed when all fail (never +// fall back to a stale local snapshot). Default chain: models.dev then the Gitee mirror of +// deepagent-ltd/models.dev (hourly-synced, reachable from CN networks). +const modelsURLs = ( + process.env.DEEPAGENT_CODE_MODELS_URL?.trim() || + "https://models.dev,https://gitee.com/deepagent-ai/models.dev/raw/main" +) + .split(",") + .map((url) => url.trim().replace(/\/$/, "")) + .filter((url) => url.length > 0) -export const modelsData = process.env.MODELS_DEV_API_JSON - ? await Bun.file(process.env.MODELS_DEV_API_JSON).text() - : await fetch(`${modelsUrl}/api.json`).then((response) => response.text()) +async function load() { + if (process.env.MODELS_DEV_API_JSON) return Bun.file(process.env.MODELS_DEV_API_JSON).text() + for (const modelsUrl of modelsURLs) { + const response = await fetch(`${modelsUrl}/api.json`).catch(() => undefined) + if (response?.ok) return response.text() + } + throw new Error(`Unable to load models.dev catalog from ${modelsURLs.join(", ")}`) +} + +export const modelsData = await load() console.log("Loaded models.dev snapshot") diff --git a/packages/cli/src/services/daemon.ts b/packages/cli/src/services/daemon.ts index 3cc3fa99..83294338 100644 --- a/packages/cli/src/services/daemon.ts +++ b/packages/cli/src/services/daemon.ts @@ -32,6 +32,12 @@ function sameRegistration(left: Registration, right: Registration) { return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid } +// PARITY-003 (Wave 0): the daemon's spawn target is switchable. DEEPAGENT_CODE_DAEMON_BACKEND=legacy +// mounts the full legacy deepagent-code server instead of the v2 `serve`; the legacy entrypoint is +// resolved from DEEPAGENT_CODE_DAEMON_LEGACY_ENTRYPOINT. Read lazily so each call observes the +// current environment. +const backend = () => (process.env.DEEPAGENT_CODE_DAEMON_BACKEND === "legacy" ? "legacy" : "v2") + export const layer = Layer.effect( Service, Effect.gen(function* () { @@ -63,8 +69,30 @@ export const layer = Layer.effect( return createOpencodeClient({ baseUrl: url, headers: ServerAuth.headers({ password: yield* password() }) }) }) + // The legacy server exposes GET /global/health -> { healthy, version, runtimeId } guarded by + // the same Basic-auth credential, instead of the v2 /api/health SDK route. + const legacyHealth = Effect.fnUntraced(function* (info: Registration) { + const credential = yield* password() + const body = yield* Effect.tryPromise({ + try: async () => { + const response = await fetch(`${info.url}/global/health`, { + headers: ServerAuth.headers({ password: credential }), + signal: AbortSignal.timeout(2_000), + }) + if (!response.ok) throw new Error(`Legacy health endpoint responded ${response.status}`) + return (await response.json()) as { healthy?: boolean; version?: string } + }, + catch: (cause) => new Error("Legacy health check failed", { cause }), + }) + if (body?.healthy !== true) return yield* Effect.fail(new Error("Registered server is not healthy")) + // The legacy endpoint reports its own version; prefer it so compatible() keeps the same + // handshake semantics as the v2 registration file. + return typeof body.version === "string" ? { ...info, version: body.version } : info + }) + const healthy = Effect.fnUntraced(function* () { const info = yield* registration() + if (backend() === "legacy") return yield* legacyHealth(info) const client = yield* createClient(info.url) const response = yield* Effect.tryPromise(() => client.v2.health.get({ signal: AbortSignal.timeout(2_000) })) if (response.data?.healthy === true) return info @@ -114,18 +142,37 @@ export const layer = Layer.effect( if (found?.version === InstallationVersion && compiled) return found.url if (found) yield* stopProcess(found).pipe(Effect.ignore) - const entrypoint = compiled ? undefined : process.argv[1] - if (!compiled && entrypoint === undefined) - return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint")) - yield* Effect.try({ - try: () => { - spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], { - detached: true, - stdio: "ignore", - }).unref() - }, - catch: (cause) => new Error("Failed to start server", { cause }), - }) + if (backend() === "legacy") { + // Wave 0: spawn the legacy deepagent-code CLI `serve --register`. The password is passed + // through the environment the legacy server already honors (DEEPAGENT_CODE_SERVER_PASSWORD). + const legacyEntrypoint = process.env.DEEPAGENT_CODE_DAEMON_LEGACY_ENTRYPOINT + if (!legacyEntrypoint) + return yield* Effect.fail(new Error("DEEPAGENT_CODE_DAEMON_LEGACY_ENTRYPOINT is required for the legacy backend")) + const credential = yield* password() + yield* Effect.try({ + try: () => { + spawn(process.execPath, [legacyEntrypoint, "serve", "--register"], { + detached: true, + stdio: "ignore", + env: { ...process.env, DEEPAGENT_CODE_SERVER_PASSWORD: credential }, + }).unref() + }, + catch: (cause) => new Error("Failed to start legacy server", { cause }), + }) + } else { + const entrypoint = compiled ? undefined : process.argv[1] + if (!compiled && entrypoint === undefined) + return yield* Effect.fail(new Error("Failed to resolve CLI entrypoint")) + yield* Effect.try({ + try: () => { + spawn(process.execPath, [...(entrypoint ? [entrypoint] : []), "serve", "--register"], { + detached: true, + stdio: "ignore", + }).unref() + }, + catch: (cause) => new Error("Failed to start server", { cause }), + }) + } return yield* compatible().pipe( Effect.retry(Schedule.spaced("50 millis").pipe(Schedule.both(Schedule.recurs(100)))), diff --git a/packages/cli/test/daemon.test.ts b/packages/cli/test/daemon.test.ts new file mode 100644 index 00000000..611855fd --- /dev/null +++ b/packages/cli/test/daemon.test.ts @@ -0,0 +1,151 @@ +// PARITY-003 (Wave 0): daemon mounting the legacy deepagent-code server. +// +// Covers the legacy-specific daemon paths without touching the v2 behavior: +// * status() resolves a legacy registration through GET /global/health (Basic auth + version) +// * an unauthenticated/unknown legacy server is treated as unhealthy and deregistered +// * start() spawns the legacy entrypoint with the password in the environment, completes the +// version handshake, and stop() tears the process down and removes the registration +import { afterAll, afterEach, beforeEach, describe, expect, it } from "bun:test" +import { InstallationVersion } from "@deepagent-code/core/installation/version" +import * as NodeServices from "@effect/platform-node/NodeServices" +import { Effect } from "effect" +import { mkdir, readFile, rm, writeFile, access } from "fs/promises" +import { mkdtemp } from "fs/promises" +import { tmpdir } from "os" +import path from "path" +import { Daemon } from "../src/services/daemon" + +const run = (effect: Effect.Effect) => + Effect.runPromise( + effect.pipe(Effect.provide(Daemon.defaultLayer), Effect.provide(NodeServices.layer), Effect.scoped), + ) + +const withDaemon = (use: (daemon: Daemon.Interface) => Effect.Effect) => + run(Effect.flatMap(Daemon.Service, use)) + +const exists = (file: string) => access(file).then( + () => true, + () => false, +) + +const fixtureEntrypoint = path.join(__dirname, "fixtures", "legacy-entry.ts") + +let home: string +const homes: string[] = [] + +const stateFile = () => path.join(home, "state", "server.json") + +beforeEach(async () => { + home = await mkdtemp(path.join(tmpdir(), "dacode-daemon-test-")) + homes.push(home) + await mkdir(path.join(home, "state"), { recursive: true }) + process.env.DEEPAGENT_CODE_TEST_HOME = home + process.env.DEEPAGENT_CODE_HOME = home + process.env.DEEPAGENT_CODE_DAEMON_BACKEND = "legacy" + delete process.env.DEEPAGENT_CODE_DAEMON_LEGACY_ENTRYPOINT + delete process.env.DEEPAGENT_CODE_DAEMON_FIXTURE_VERSION +}) + +afterEach(() => { + delete process.env.DEEPAGENT_CODE_DAEMON_BACKEND + delete process.env.DEEPAGENT_CODE_DAEMON_LEGACY_ENTRYPOINT + delete process.env.DEEPAGENT_CODE_DAEMON_FIXTURE_VERSION +}) + +afterAll(async () => { + delete process.env.DEEPAGENT_CODE_HOME + delete process.env.DEEPAGENT_CODE_TEST_HOME + await Promise.all(homes.map((dir) => rm(dir, { recursive: true, force: true }))) +}) + +function fakeLegacyServer(version: string, expectedPassword?: () => string) { + let seenAuth: string | undefined + const server = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + fetch(request) { + const url = new URL(request.url) + if (url.pathname === "/global/health") { + seenAuth = request.headers.get("authorization") ?? undefined + const expected = `Basic ${Buffer.from(`deepagent-code:${expectedPassword?.() ?? ""}`).toString("base64")}` + if (seenAuth !== expected) return Response.json({ error: "unauthorized" }, { status: 401 }) + return Response.json({ healthy: true, version, runtimeId: "fake" }) + } + return Response.json({ error: "not found" }, { status: 404 }) + }, + }) + return { server, url: `http://127.0.0.1:${server.port}`, seenAuth: () => seenAuth } +} + +describe("daemon legacy backend (PARITY-003)", () => { + it("resolves a legacy registration through /global/health with Basic auth", async () => { + let password = "" + const fake = fakeLegacyServer(InstallationVersion, () => password) + try { + password = await withDaemon((daemon) => daemon.password()) + await writeFile( + stateFile(), + JSON.stringify({ id: "fixed", version: InstallationVersion, url: fake.url, pid: process.pid }), + ) + const url = await withDaemon((daemon) => daemon.status()) + expect(url).toBe(fake.url) + expect(fake.seenAuth()).toBe(`Basic ${Buffer.from(`deepagent-code:${password}`).toString("base64")}`) + } finally { + fake.server.stop() + } + }) + + it("treats an unauthenticated legacy server as unhealthy and clears the registration", async () => { + // The fake demands a password the daemon does not know (none generated yet on its side; + // generate one that differs from the fake's expected credential). + const fake = fakeLegacyServer(InstallationVersion, () => "some-other-secret") + try { + await withDaemon((daemon) => daemon.password("daemon-secret")) + await writeFile( + stateFile(), + JSON.stringify({ id: "fixed", version: InstallationVersion, url: fake.url, pid: process.pid }), + ) + const url = await withDaemon((daemon) => daemon.status()) + expect(url).toBeUndefined() + expect(await exists(stateFile())).toBe(false) + } finally { + fake.server.stop() + } + }) + + it("start() spawns the legacy entrypoint, completes the handshake, and stop() tears it down", async () => { + process.env.DEEPAGENT_CODE_DAEMON_LEGACY_ENTRYPOINT = fixtureEntrypoint + process.env.DEEPAGENT_CODE_DAEMON_FIXTURE_VERSION = InstallationVersion + + const url = await withDaemon((daemon) => daemon.start()) + expect(url).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/) + + const registration = JSON.parse(await readFile(stateFile(), "utf8")) + expect(registration.version).toBe(InstallationVersion) + expect(registration.url).toBe(url) + expect(registration.pid).toBeGreaterThan(0) + + // The spawned server received the daemon's password and authenticates health checks with it. + const password = await withDaemon((daemon) => daemon.password()) + const health = await fetch(`${url}/global/health`, { + headers: { Authorization: `Basic ${Buffer.from(`deepagent-code:${password}`).toString("base64")}` }, + }) + expect(health.status).toBe(200) + expect(await health.json()).toMatchObject({ healthy: true, version: InstallationVersion }) + + await withDaemon((daemon) => daemon.stop()) + expect(await exists(stateFile())).toBe(false) + + // The spawned process stepped down on SIGTERM. + const pid = registration.pid as number + for (let i = 0; i < 100; i++) { + try { + process.kill(pid, 0) + } catch { + return + } + await new Promise((resolve) => setTimeout(resolve, 50)) + } + throw new Error(`Legacy server process ${pid} is still running after stop()`) + }, 15_000) +}) diff --git a/packages/cli/test/fixtures/legacy-entry.ts b/packages/cli/test/fixtures/legacy-entry.ts new file mode 100644 index 00000000..fc67936f --- /dev/null +++ b/packages/cli/test/fixtures/legacy-entry.ts @@ -0,0 +1,62 @@ +// PARITY-003 (Wave 0) fixture: a minimal stand-in for the legacy deepagent-code CLI entrypoint. +// +// Implements exactly the protocol surface the daemon relies on when mounting a legacy server: +// * `serve --register` listens on an ephemeral port and exposes GET /global/health guarded by +// Basic auth (the same DEEPAGENT_CODE_SERVER_PASSWORD credential the daemon injects). +// * Writes state/server.json ({ id, version, url, pid }) atomically under the isolated test +// home, mirroring registerWithDaemon in packages/deepagent-code/src/cli/cmd/serve.ts. +// * Steps down on SIGTERM: removes the registration it owns, then exits. +// +// Plain Node/Bun on purpose — importing the real legacy CLI would drag in the whole app runtime. +import { randomUUID } from "node:crypto" +import fsSync from "node:fs" +import path from "node:path" + +const version = process.env.DEEPAGENT_CODE_DAEMON_FIXTURE_VERSION ?? "local" +const stateDir = path.join(process.env.DEEPAGENT_CODE_HOME ?? process.cwd(), "state") +const file = path.join(stateDir, "server.json") + +const read = (): { id?: string } | undefined => { + try { + return JSON.parse(fsSync.readFileSync(file, "utf8")) + } catch { + return undefined + } +} +const id = randomUUID() +const owned = () => read()?.id === id +const expectedAuth = `Basic ${Buffer.from(`deepagent-code:${process.env.DEEPAGENT_CODE_SERVER_PASSWORD ?? ""}`).toString("base64")}` + +const server = Bun.serve({ + port: 0, + hostname: "127.0.0.1", + fetch(request) { + const url = new URL(request.url) + if (url.pathname === "/global/health") { + if (request.headers.get("authorization") !== expectedAuth) { + return Response.json({ error: "unauthorized" }, { status: 401 }) + } + return Response.json({ healthy: true, version, runtimeId: "fixture" }) + } + return Response.json({ error: "not found" }, { status: 404 }) + }, +}) + +fsSync.mkdirSync(stateDir, { recursive: true }) +const temp = `${file}.${id}.tmp` +fsSync.writeFileSync(temp, JSON.stringify({ id, version, url: `http://127.0.0.1:${server.port}`, pid: process.pid }), { + mode: 0o600, +}) +fsSync.renameSync(temp, file) + +process.on("SIGTERM", () => { + if (owned()) { + try { + fsSync.unlinkSync(file) + } catch { + // A concurrent takeover may have already replaced the file. + } + } + server.stop(true) + process.exit(0) +}) diff --git a/packages/core/migration/20260816073717_session_facade_activity/migration.sql b/packages/core/migration/20260816073717_session_facade_activity/migration.sql new file mode 100644 index 00000000..e26c4f2a --- /dev/null +++ b/packages/core/migration/20260816073717_session_facade_activity/migration.sql @@ -0,0 +1,20 @@ +CREATE TABLE `session_facade_activity` ( + `activity_id` text PRIMARY KEY, + `subkind` text NOT NULL, + `parent_session_id` text NOT NULL, + `owner_session_id` text, + `spawn_tool_call_id` text, + `objective_text` text, + `budget_json` text, + `state` text NOT NULL, + `reason_code` text, + `source` text, + `created_at` integer NOT NULL, + `settled_at` integer, + `mutation_epoch` integer NOT NULL, + CONSTRAINT `fk_session_facade_activity_parent_session_id_session_id_fk` FOREIGN KEY (`parent_session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_session_facade_activity_owner_session_id_session_id_fk` FOREIGN KEY (`owner_session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE +); +--> statement-breakpoint +CREATE UNIQUE INDEX `session_facade_activity_active_idx` ON `session_facade_activity` (`parent_session_id`,`subkind`) WHERE "session_facade_activity"."state" = 'active';--> statement-breakpoint +CREATE INDEX `session_facade_activity_parent_idx` ON `session_facade_activity` (`parent_session_id`,`state`,`created_at`); \ No newline at end of file diff --git a/packages/core/migration/20260816073717_session_facade_activity/snapshot.json b/packages/core/migration/20260816073717_session_facade_activity/snapshot.json new file mode 100644 index 00000000..8a38b8fb --- /dev/null +++ b/packages/core/migration/20260816073717_session_facade_activity/snapshot.json @@ -0,0 +1,16233 @@ +{ + "version": "7", + "dialect": "sqlite", + "id": "29b4297e-5c48-49d6-9ea2-61cdd1e56475", + "prevIds": [ + "0a9c6817-9da1-42d9-9c11-86d9fd55fae8" + ], + "ddl": [ + { + "name": "workspace", + "entityType": "tables" + }, + { + "name": "data_migration", + "entityType": "tables" + }, + { + "name": "session_activity_effect_receipt", + "entityType": "tables" + }, + { + "name": "session_activity_evidence", + "entityType": "tables" + }, + { + "name": "session_activity_objective", + "entityType": "tables" + }, + { + "name": "session_activity_permission_decision", + "entityType": "tables" + }, + { + "name": "session_activity_permission_effect_dispatch", + "entityType": "tables" + }, + { + "name": "session_activity_permission_once_consumption", + "entityType": "tables" + }, + { + "name": "session_activity_permission_owner_lease", + "entityType": "tables" + }, + { + "name": "session_activity_permission_request", + "entityType": "tables" + }, + { + "name": "session_activity_progress_observation", + "entityType": "tables" + }, + { + "name": "session_facade_activity", + "entityType": "tables" + }, + { + "name": "learning_admission_outbox", + "entityType": "tables" + }, + { + "name": "learning_governance_action", + "entityType": "tables" + }, + { + "name": "learning_governance_compensation", + "entityType": "tables" + }, + { + "name": "learning_governance_plan", + "entityType": "tables" + }, + { + "name": "learning_job", + "entityType": "tables" + }, + { + "name": "learning_lifecycle_trigger_receipt", + "entityType": "tables" + }, + { + "name": "learning_reviewer_attempt", + "entityType": "tables" + }, + { + "name": "released_knowledge_evaluation", + "entityType": "tables" + }, + { + "name": "released_knowledge_snapshot_document", + "entityType": "tables" + }, + { + "name": "released_knowledge_snapshot_head", + "entityType": "tables" + }, + { + "name": "released_knowledge_snapshot", + "entityType": "tables" + }, + { + "name": "file_part_artifact_binding", + "entityType": "tables" + }, + { + "name": "file_part_artifact_chunk", + "entityType": "tables" + }, + { + "name": "file_part_artifact_discard", + "entityType": "tables" + }, + { + "name": "file_part_artifact_import", + "entityType": "tables" + }, + { + "name": "file_part_artifact", + "entityType": "tables" + }, + { + "name": "session_v2_provider_parity_baseline", + "entityType": "tables" + }, + { + "name": "session_v2_provider_parity_receipt", + "entityType": "tables" + }, + { + "name": "session_v2_provider_turn_receipt", + "entityType": "tables" + }, + { + "name": "session_transfer_operation", + "entityType": "tables" + }, + { + "name": "session_transfer_target_receipt", + "entityType": "tables" + }, + { + "name": "account_state", + "entityType": "tables" + }, + { + "name": "account", + "entityType": "tables" + }, + { + "name": "control_account", + "entityType": "tables" + }, + { + "name": "context_location_identity_alias", + "entityType": "tables" + }, + { + "name": "context_location_identity", + "entityType": "tables" + }, + { + "name": "location_index_coordination", + "entityType": "tables" + }, + { + "name": "context_project_scope_identity_alias", + "entityType": "tables" + }, + { + "name": "context_project_scope_identity", + "entityType": "tables" + }, + { + "name": "context_security_namespace", + "entityType": "tables" + }, + { + "name": "event_artifact_chunk", + "entityType": "tables" + }, + { + "name": "event_artifact", + "entityType": "tables" + }, + { + "name": "event_compaction_receipt", + "entityType": "tables" + }, + { + "name": "event_dedupe", + "entityType": "tables" + }, + { + "name": "event_sequence", + "entityType": "tables" + }, + { + "name": "event_snapshot_attempt", + "entityType": "tables" + }, + { + "name": "event_snapshot_chunk", + "entityType": "tables" + }, + { + "name": "event_snapshot_row", + "entityType": "tables" + }, + { + "name": "event_snapshot", + "entityType": "tables" + }, + { + "name": "event_sync_backfill", + "entityType": "tables" + }, + { + "name": "event_sync_index", + "entityType": "tables" + }, + { + "name": "event_sync_sequence", + "entityType": "tables" + }, + { + "name": "event", + "entityType": "tables" + }, + { + "name": "workspace_sync_cursor", + "entityType": "tables" + }, + { + "name": "im_attachments", + "entityType": "tables" + }, + { + "name": "im_groups", + "entityType": "tables" + }, + { + "name": "im_members", + "entityType": "tables" + }, + { + "name": "im_messages", + "entityType": "tables" + }, + { + "name": "location_change_event", + "entityType": "tables" + }, + { + "name": "location_projection_dirty_path", + "entityType": "tables" + }, + { + "name": "location_projection_registration", + "entityType": "tables" + }, + { + "name": "permission_saved_epoch", + "entityType": "tables" + }, + { + "name": "permission", + "entityType": "tables" + }, + { + "name": "project_directory", + "entityType": "tables" + }, + { + "name": "project", + "entityType": "tables" + }, + { + "name": "message", + "entityType": "tables" + }, + { + "name": "part", + "entityType": "tables" + }, + { + "name": "session_context_epoch", + "entityType": "tables" + }, + { + "name": "session_fork_admission", + "entityType": "tables" + }, + { + "name": "session_fork_intent", + "entityType": "tables" + }, + { + "name": "session_history_state", + "entityType": "tables" + }, + { + "name": "session_input", + "entityType": "tables" + }, + { + "name": "session_intent", + "entityType": "tables" + }, + { + "name": "session_message", + "entityType": "tables" + }, + { + "name": "session_part_integrity_quarantine", + "entityType": "tables" + }, + { + "name": "session_prompt_epoch_message", + "entityType": "tables" + }, + { + "name": "session_prompt_epoch_recovery", + "entityType": "tables" + }, + { + "name": "session_steer", + "entityType": "tables" + }, + { + "name": "session", + "entityType": "tables" + }, + { + "name": "session_tool_request_resolution_command", + "entityType": "tables" + }, + { + "name": "session_tool_request_resolution", + "entityType": "tables" + }, + { + "name": "session_world_state_baseline", + "entityType": "tables" + }, + { + "name": "task_admission", + "entityType": "tables" + }, + { + "name": "task_notification_outbox", + "entityType": "tables" + }, + { + "name": "task_run_event", + "entityType": "tables" + }, + { + "name": "task_run", + "entityType": "tables" + }, + { + "name": "task_structured_finalizer_response", + "entityType": "tables" + }, + { + "name": "task_structured_output_evidence_part", + "entityType": "tables" + }, + { + "name": "task_structured_output_evidence", + "entityType": "tables" + }, + { + "name": "todo", + "entityType": "tables" + }, + { + "name": "session_share", + "entityType": "tables" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "''", + "generated": null, + "name": "name", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "branch", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "extra", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_used", + "entityType": "columns", + "table": "workspace" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "data_migration" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_kind", + "entityType": "columns", + "table": "session_activity_effect_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_id", + "entityType": "columns", + "table": "session_activity_effect_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "receipt_id", + "entityType": "columns", + "table": "session_activity_effect_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "effect_fingerprint", + "entityType": "columns", + "table": "session_activity_effect_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "first_observation_revision", + "entityType": "columns", + "table": "session_activity_effect_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_activity_effect_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_kind", + "entityType": "columns", + "table": "session_activity_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_id", + "entityType": "columns", + "table": "session_activity_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "evidence_fingerprint", + "entityType": "columns", + "table": "session_activity_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "evidence_kind", + "entityType": "columns", + "table": "session_activity_evidence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_receipt_id", + "entityType": "columns", + "table": "session_activity_evidence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "first_observation_revision", + "entityType": "columns", + "table": "session_activity_evidence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_activity_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_kind", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_id", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admission_fingerprint", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "objective_fingerprint", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "objective_text", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "completion_criteria", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "enforcement_state", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "stall_threshold", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "no_progress_count", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "latest_observation_revision", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "latest_vector_hash", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "next_action", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "terminal_reason", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "session_activity_objective" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "decision_id", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_id", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "idempotency_key", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "decision", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_type", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "scope", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "authority_epoch", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "decided_at", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "feedback", + "entityType": "columns", + "table": "session_activity_permission_decision" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "receipt_id", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_id", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_kind", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_id", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "tool_message_id", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "tool_call_id", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "tool_name", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "consumer_id", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "idempotency_key", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "outcome", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_json", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_hash", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "started_at", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "session_activity_permission_effect_dispatch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_id", + "entityType": "columns", + "table": "session_activity_permission_once_consumption" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "consumer_id", + "entityType": "columns", + "table": "session_activity_permission_once_consumption" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "idempotency_key", + "entityType": "columns", + "table": "session_activity_permission_once_consumption" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "consumed_at", + "entityType": "columns", + "table": "session_activity_permission_once_consumption" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "session_activity_permission_owner_lease" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "lease_expires_at", + "entityType": "columns", + "table": "session_activity_permission_owner_lease" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "heartbeat_at", + "entityType": "columns", + "table": "session_activity_permission_owner_lease" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_id", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_kind", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_id", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_kind", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "idempotency_key", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "patterns", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "always_patterns", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata_hash", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "tool_message_id", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "tool_call_id", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "authority_epoch", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "requested_scope", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_type", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "decided_at", + "entityType": "columns", + "table": "session_activity_permission_request" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_kind", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_id", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revision", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "idempotency_key", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "observation_fingerprint", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expected_objective_version", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_revision", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "plan_version", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "validation_fingerprint", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "evidence_set_hash", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "effect_receipt_set_hash", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vector_hash", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "next_action", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "changed", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "no_progress_count", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "observed_at", + "entityType": "columns", + "table": "session_activity_progress_observation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activity_id", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "subkind", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_session_id", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_session_id", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "spawn_tool_call_id", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "objective_text", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "budget_json", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reason_code", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mutation_epoch", + "entityType": "columns", + "table": "session_facade_activity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "intent_id", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "run_id", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "trigger", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "dedupe_key", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "payload_json", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "payload_fingerprint", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "job_id", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "candidate_input_ref", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "rejection_code", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "rejection_detail", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "learning_admission_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action_id", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "plan_id", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "candidate_id", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sequence", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "kind", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "predecessor_action_id", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "payload_json", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "payload_fingerprint", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "lease_expires_at", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_ref", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_hash", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_fingerprint", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_code", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_detail", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "learning_governance_action" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "compensation_id", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "plan_id", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action_id", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sequence", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "kind", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_payload_fingerprint", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "lease_expires_at", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_ref", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_hash", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_fingerprint", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_code", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_detail", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "learning_governance_compensation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "plan_id", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "job_id", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "policy", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "payload_json", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "payload_fingerprint", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action_count", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "job_owner", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_job_version", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "job_started_version", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_ref", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_hash", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_fingerprint", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_code", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_detail", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "learning_governance_plan" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "job_id", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "run_id", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "trigger", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "dedupe_key", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "candidate_input_ref", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "policy", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "max_attempts", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admission_fingerprint", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "attempts", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "lease_expires_at", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "side_effect_state", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "side_effect_kind", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expected_result_ref", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "review_job_id", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_ref", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_code", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_detail", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settlement_fingerprint", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "next_attempt_at", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "started_at", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "learning_job" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "receipt_id", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "trigger", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "boundary_key", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "run_id", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_admission_hash", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_terminal_hash", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "artifact_path", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "artifact_hash", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "artifact_json", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admission_fingerprint", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admission_json", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_detail", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "attempt_id", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "job_id", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "review_session_id", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_ref", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_hash", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_candidate_ids_json", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_candidate_set_hash", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "provider_id", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model_id", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "policy_hash", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "response_ref", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "response_hash", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "verdict", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "selected_candidate_ids_json", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "selected_subset_hash", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_code", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_detail", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "dispatched_at", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "learning_reviewer_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "evaluation_id", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "security_namespace_id", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_scope_key", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "matrix_hash", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "matrix_json", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "document_manifest_json", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline_ref", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "repetitions", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "evaluator_type", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "evaluator_id", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "released_knowledge_evaluation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_id", + "entityType": "columns", + "table": "released_knowledge_snapshot_document" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ordinal", + "entityType": "columns", + "table": "released_knowledge_snapshot_document" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_store", + "entityType": "columns", + "table": "released_knowledge_snapshot_document" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "doc_id", + "entityType": "columns", + "table": "released_knowledge_snapshot_document" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "doc_version", + "entityType": "columns", + "table": "released_knowledge_snapshot_document" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "doc_hash", + "entityType": "columns", + "table": "released_knowledge_snapshot_document" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "doc_type", + "entityType": "columns", + "table": "released_knowledge_snapshot_document" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "doc_scope", + "entityType": "columns", + "table": "released_knowledge_snapshot_document" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "security_namespace_id", + "entityType": "columns", + "table": "released_knowledge_snapshot_head" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_scope_key", + "entityType": "columns", + "table": "released_knowledge_snapshot_head" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_id", + "entityType": "columns", + "table": "released_knowledge_snapshot_head" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "generation", + "entityType": "columns", + "table": "released_knowledge_snapshot_head" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "released_knowledge_snapshot_head" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_id", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "security_namespace_id", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_scope_key", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "legacy_project_id", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_snapshot_id", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "evaluation_id", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "release_kind", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "document_count", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "published_generation", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "verdict", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "failure_reason", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_type", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "finalized_at", + "entityType": "columns", + "table": "released_knowledge_snapshot" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "event_id", + "entityType": "columns", + "table": "file_part_artifact_binding" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "file_part_artifact_binding" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "file_part_artifact_binding" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "part_id", + "entityType": "columns", + "table": "file_part_artifact_binding" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "artifact_id", + "entityType": "columns", + "table": "file_part_artifact_binding" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "original_data_hash", + "entityType": "columns", + "table": "file_part_artifact_binding" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "canonical_data_hash", + "entityType": "columns", + "table": "file_part_artifact_binding" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "canonical_data", + "entityType": "columns", + "table": "file_part_artifact_binding" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "file_part_artifact_binding" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "artifact_id", + "entityType": "columns", + "table": "file_part_artifact_chunk" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chunk_index", + "entityType": "columns", + "table": "file_part_artifact_chunk" + }, + { + "type": "blob", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "file_part_artifact_chunk" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chunk_hash", + "entityType": "columns", + "table": "file_part_artifact_chunk" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "event_id", + "entityType": "columns", + "table": "file_part_artifact_discard" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "file_part_artifact_discard" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "file_part_artifact_discard" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "artifact_id", + "entityType": "columns", + "table": "file_part_artifact_discard" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "original_data_hash", + "entityType": "columns", + "table": "file_part_artifact_discard" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "canonical_data_hash", + "entityType": "columns", + "table": "file_part_artifact_discard" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "canonical_data", + "entityType": "columns", + "table": "file_part_artifact_discard" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "file_part_artifact_discard" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "event_id", + "entityType": "columns", + "table": "file_part_artifact_import" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "file_part_artifact_import" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "file_part_artifact_import" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "artifact_id", + "entityType": "columns", + "table": "file_part_artifact_import" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "original_data_hash", + "entityType": "columns", + "table": "file_part_artifact_import" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "canonical_data_hash", + "entityType": "columns", + "table": "file_part_artifact_import" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "canonical_data", + "entityType": "columns", + "table": "file_part_artifact_import" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "file_part_artifact_import" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "artifact_id", + "entityType": "columns", + "table": "file_part_artifact" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "body_hash", + "entityType": "columns", + "table": "file_part_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "body_bytes", + "entityType": "columns", + "table": "file_part_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chunk_bytes", + "entityType": "columns", + "table": "file_part_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chunk_count", + "entityType": "columns", + "table": "file_part_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "codec_version", + "entityType": "columns", + "table": "file_part_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "complete", + "entityType": "columns", + "table": "file_part_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "file_part_artifact" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "campaign_id", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "case_name", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "legacy_receipt_id", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prepared_turn", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "outcome_hash", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "outcome_artifact", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "legacy_response_fingerprint", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "evidence", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "receipt_hash", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "settled_at", + "entityType": "columns", + "table": "session_v2_provider_parity_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "campaign_id", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "case_name", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "legacy_receipt_id", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "core_v2_receipt_id", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "legacy_request_hash", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "core_v2_request_hash", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "legacy_outcome_hash", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "core_v2_outcome_hash", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "legacy_prepared_turn", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "core_v2_prepared_turn", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "diff_artifact", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "allowlist_version", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "allowlisted_differences", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "disallowed_differences", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "evidence", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "verified", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "receipt_hash", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_v2_provider_parity_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "receipt_id", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_ordinal", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "user_message_id", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "history_prompt_epoch", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "history_source_end_message_id", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_input_hash", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "provider_id", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model_id", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "protocol", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_mode", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_token", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prepared_turn_hash", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "wire_request_hash", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prepared_turn", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "outcome_hash", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "outcome_artifact", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_code", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "dispatching_at", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "first_event_at", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "terminal_at", + "entityType": "columns", + "table": "session_v2_provider_turn_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "transfer_id", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_workspace_id", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "target_workspace_id", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_owner_id", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "target_owner_id", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_event_seq", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_mutation_epoch", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_id", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_hash", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_hash", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error_code", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "completed_at", + "entityType": "columns", + "table": "session_transfer_operation" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "transfer_id", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_snapshot_id", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_snapshot_hash", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_event_seq", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "target_workspace_id", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "target_owner_id", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activated_snapshot_id", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "activated_at", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_transfer_target_receipt" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_account_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active_org_id", + "entityType": "columns", + "table": "account_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "email", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "access_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "refresh_token", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "token_expiry", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "active", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "control_account" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "security_namespace_id", + "entityType": "columns", + "table": "context_location_identity_alias" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "old_canonical_root", + "entityType": "columns", + "table": "context_location_identity_alias" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "location_key", + "entityType": "columns", + "table": "context_location_identity_alias" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reason", + "entityType": "columns", + "table": "context_location_identity_alias" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "context_location_identity_alias" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "security_namespace_id", + "entityType": "columns", + "table": "context_location_identity" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "location_key", + "entityType": "columns", + "table": "context_location_identity" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_scope_key", + "entityType": "columns", + "table": "context_location_identity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_binding", + "entityType": "columns", + "table": "context_location_identity" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "canonical_root", + "entityType": "columns", + "table": "context_location_identity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "observed_project_id", + "entityType": "columns", + "table": "context_location_identity" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "context_location_identity" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "retired_at", + "entityType": "columns", + "table": "context_location_identity" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "security_namespace_id", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "location_key", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "index_space_id", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "projection_kind", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "index_incarnation", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "db_locator", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "fencing_token", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expires_at", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "replacement_state", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "location_index_coordination" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "security_namespace_id", + "entityType": "columns", + "table": "context_project_scope_identity_alias" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "old_project_identity_hash", + "entityType": "columns", + "table": "context_project_scope_identity_alias" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_scope_key", + "entityType": "columns", + "table": "context_project_scope_identity_alias" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reason", + "entityType": "columns", + "table": "context_project_scope_identity_alias" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "context_project_scope_identity_alias" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "security_namespace_id", + "entityType": "columns", + "table": "context_project_scope_identity" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_scope_key", + "entityType": "columns", + "table": "context_project_scope_identity" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_kind", + "entityType": "columns", + "table": "context_project_scope_identity" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_identity_hash", + "entityType": "columns", + "table": "context_project_scope_identity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "observed_project_id", + "entityType": "columns", + "table": "context_project_scope_identity" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "context_project_scope_identity" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "retired_at", + "entityType": "columns", + "table": "context_project_scope_identity" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "context_security_namespace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "kind", + "entityType": "columns", + "table": "context_security_namespace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "binding_hash", + "entityType": "columns", + "table": "context_security_namespace" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "context_security_namespace" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "retired_at", + "entityType": "columns", + "table": "context_security_namespace" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "artifact_id", + "entityType": "columns", + "table": "event_artifact_chunk" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chunk_index", + "entityType": "columns", + "table": "event_artifact_chunk" + }, + { + "type": "blob", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event_artifact_chunk" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chunk_hash", + "entityType": "columns", + "table": "event_artifact_chunk" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "artifact_id", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "event_id", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "kind", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "original_data_hash", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "canonical_data_hash", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "canonical_data", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "body_hash", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "body_bytes", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chunk_count", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "codec_version", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "event_artifact" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_compaction_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_id", + "entityType": "columns", + "table": "event_compaction_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "through_seq", + "entityType": "columns", + "table": "event_compaction_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "codec", + "entityType": "columns", + "table": "event_compaction_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "schema_version", + "entityType": "columns", + "table": "event_compaction_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "cursor_seq", + "entityType": "columns", + "table": "event_compaction_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "deleted_count", + "entityType": "columns", + "table": "event_compaction_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "event_compaction_receipt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "event_compaction_receipt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_dedupe" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_dedupe" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "event_id", + "entityType": "columns", + "table": "event_dedupe" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event_dedupe" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data_hash", + "entityType": "columns", + "table": "event_dedupe" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_data", + "entityType": "columns", + "table": "event_dedupe" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "compacted_at", + "entityType": "columns", + "table": "event_dedupe" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "retention_floor_seq", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "write_fence_transfer_id", + "entityType": "columns", + "table": "event_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_id", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "through_seq", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expected_latest", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "codec", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "schema_version", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "projection_revision", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "cursor", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "row_count", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "encoded_bytes", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content_hash", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "tables", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "event_snapshot_attempt" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "row_hash", + "entityType": "columns", + "table": "event_snapshot_chunk" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chunk_index", + "entityType": "columns", + "table": "event_snapshot_chunk" + }, + { + "type": "blob", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event_snapshot_chunk" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chunk_hash", + "entityType": "columns", + "table": "event_snapshot_chunk" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_id", + "entityType": "columns", + "table": "event_snapshot_row" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_snapshot_row" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "row_index", + "entityType": "columns", + "table": "event_snapshot_row" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "table_name", + "entityType": "columns", + "table": "event_snapshot_row" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "row_key", + "entityType": "columns", + "table": "event_snapshot_row" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "row_hash", + "entityType": "columns", + "table": "event_snapshot_row" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "row_bytes", + "entityType": "columns", + "table": "event_snapshot_row" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chunk_count", + "entityType": "columns", + "table": "event_snapshot_row" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "chain_hash", + "entityType": "columns", + "table": "event_snapshot_row" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_id", + "entityType": "columns", + "table": "event_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_snapshot" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "through_seq", + "entityType": "columns", + "table": "event_snapshot" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sync_seq", + "entityType": "columns", + "table": "event_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "codec", + "entityType": "columns", + "table": "event_snapshot" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "schema_version", + "entityType": "columns", + "table": "event_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot_hash", + "entityType": "columns", + "table": "event_snapshot" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "body", + "entityType": "columns", + "table": "event_snapshot" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_id", + "entityType": "columns", + "table": "event_snapshot" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "event_snapshot" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event_sync_backfill" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "event_sync_backfill" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "cursor_rowid", + "entityType": "columns", + "table": "event_sync_backfill" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "high_water_rowid", + "entityType": "columns", + "table": "event_sync_backfill" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "processed_count", + "entityType": "columns", + "table": "event_sync_backfill" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "started_at", + "entityType": "columns", + "table": "event_sync_backfill" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "event_sync_backfill" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "completed_at", + "entityType": "columns", + "table": "event_sync_backfill" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sync_seq", + "entityType": "columns", + "table": "event_sync_index" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "event_id", + "entityType": "columns", + "table": "event_sync_index" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event_sync_index" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sync_index" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event_sync_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event_sync_sequence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "generation", + "entityType": "columns", + "table": "event_sync_sequence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "cursor_secret", + "entityType": "columns", + "table": "event_sync_sequence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "backfill_complete", + "entityType": "columns", + "table": "event_sync_sequence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "aggregate_id", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "event" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sync_seq", + "entityType": "columns", + "table": "event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "workspace_sync_cursor" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "remote_fingerprint", + "entityType": "columns", + "table": "workspace_sync_cursor" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "cursor", + "entityType": "columns", + "table": "workspace_sync_cursor" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "workspace_sync_cursor" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "group_id", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "uploaded_by", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "storage_path", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "filename", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mime", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "size_bytes", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "checksum", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "deleted_at", + "entityType": "columns", + "table": "im_attachments" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "im_groups" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "im_groups" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "im_groups" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "im_groups" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "im_groups" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_by", + "entityType": "columns", + "table": "im_groups" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "im_groups" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "im_groups" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "deleted_at", + "entityType": "columns", + "table": "im_groups" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "group_id", + "entityType": "columns", + "table": "im_members" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "member_id", + "entityType": "columns", + "table": "im_members" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "member_type", + "entityType": "columns", + "table": "im_members" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "im_members" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_read_at", + "entityType": "columns", + "table": "im_members" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "joined_at", + "entityType": "columns", + "table": "im_members" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "group_id", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sender_id", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sender_type", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "mentions", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reply_to_id", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "event_id", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery_status", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "deleted_at", + "entityType": "columns", + "table": "im_messages" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "event_seq", + "entityType": "columns", + "table": "location_change_event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "index_space_id", + "entityType": "columns", + "table": "location_change_event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "location_change_event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "previous_path", + "entityType": "columns", + "table": "location_change_event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "rename_correlation_id", + "entityType": "columns", + "table": "location_change_event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "change_kind", + "entityType": "columns", + "table": "location_change_event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "observed_mtime_ns", + "entityType": "columns", + "table": "location_change_event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "observed_sha", + "entityType": "columns", + "table": "location_change_event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source", + "entityType": "columns", + "table": "location_change_event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "observed_at", + "entityType": "columns", + "table": "location_change_event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "index_space_id", + "entityType": "columns", + "table": "location_projection_dirty_path" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "projection_kind", + "entityType": "columns", + "table": "location_projection_dirty_path" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "location_projection_dirty_path" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "latest_event_seq", + "entityType": "columns", + "table": "location_projection_dirty_path" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "previous_path", + "entityType": "columns", + "table": "location_projection_dirty_path" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "rename_correlation_id", + "entityType": "columns", + "table": "location_projection_dirty_path" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "change_kind", + "entityType": "columns", + "table": "location_projection_dirty_path" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "observed_mtime_ns", + "entityType": "columns", + "table": "location_projection_dirty_path" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "observed_sha", + "entityType": "columns", + "table": "location_projection_dirty_path" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "location_projection_dirty_path" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "index_space_id", + "entityType": "columns", + "table": "location_projection_registration" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "projection_kind", + "entityType": "columns", + "table": "location_projection_registration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "registration_epoch", + "entityType": "columns", + "table": "location_projection_registration" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "location_projection_registration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "consumed_event_seq", + "entityType": "columns", + "table": "location_projection_registration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reconcile_required", + "entityType": "columns", + "table": "location_projection_registration" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "location_projection_registration" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission_saved_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "epoch", + "entityType": "columns", + "table": "permission_saved_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "updated_at", + "entityType": "columns", + "table": "permission_saved_epoch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "action", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resource", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "permission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "permission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project_directory" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "vcs", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "name", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_url_override", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "icon_color", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "project" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_initialized", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sandboxes", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "commands", + "entityType": "columns", + "table": "project" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "provenance", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'auto'", + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "baseline_seq", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "replacement_seq", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "revision", + "entityType": "columns", + "table": "session_context_epoch" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "intent_id", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_hash", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "fork_mode", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_session_id", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_prompt_epoch", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_window_id", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_effective_history_hash", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_mutation_epoch", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_message_count", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_cutoff_message_id", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "projection_version", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sanitation_policy_version", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "requested_directory", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "isolation_mode", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "requested_target_session_id", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "target_session_id", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "child_depth", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "task_request_hash", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree_directory", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree_branch", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree_base_commit", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "recovery_reason", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_fork_admission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "intent_id", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_hash", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "fork_mode", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_session_id", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_prompt_epoch", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_window_id", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_effective_history_hash", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_mutation_epoch", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_message_count", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_cutoff_message_id", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "projection_version", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "sanitation_policy_version", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "target_session_id", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "target_prompt_epoch", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "target_window_id", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "target_effective_history_hash", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "target_world_state_baseline_hash", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "cloned_message_count", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "cloned_part_count", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "event_cursor", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "event_count", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery_owner", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "lease_expires_at", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "delivery_attempts", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "recovery_reason", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_committed", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_completed", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "side_effects_completed_at", + "entityType": "columns", + "table": "session_fork_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_history_state" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_history_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reason", + "entityType": "columns", + "table": "session_history_state" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_history_state" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_history_state" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admitted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "promoted_seq", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_input" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "intent_id", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "selected_variant", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "selected_payload_hash", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admitted_message_id", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "correlation_id", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_token", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "lease_expires_at", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'legacy'", + "generated": null, + "name": "execution_mode", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'legacy'", + "generated": null, + "name": "execution_state", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "execution_claim_id", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "execution_claimed_at", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "mutation_epoch", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_selected", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_admitted", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_intent" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "session_message" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "part_id", + "entityType": "columns", + "table": "session_part_integrity_quarantine" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "session_part_integrity_quarantine" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "part_session_id", + "entityType": "columns", + "table": "session_part_integrity_quarantine" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_session_id", + "entityType": "columns", + "table": "session_part_integrity_quarantine" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reason", + "entityType": "columns", + "table": "session_part_integrity_quarantine" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "quarantined_at", + "entityType": "columns", + "table": "session_part_integrity_quarantine" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_prompt_epoch_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt_epoch", + "entityType": "columns", + "table": "session_prompt_epoch_message" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ordinal", + "entityType": "columns", + "table": "session_prompt_epoch_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "session_prompt_epoch_message" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_prompt_epoch_recovery" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt_epoch", + "entityType": "columns", + "table": "session_prompt_epoch_recovery" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resolution_id", + "entityType": "columns", + "table": "session_prompt_epoch_recovery" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_prompt_epoch", + "entityType": "columns", + "table": "session_prompt_epoch_recovery" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_mutation_epoch", + "entityType": "columns", + "table": "session_prompt_epoch_recovery" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "successor_mutation_epoch", + "entityType": "columns", + "table": "session_prompt_epoch_recovery" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ambiguity_message_id", + "entityType": "columns", + "table": "session_prompt_epoch_recovery" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "physical_message_high_water", + "entityType": "columns", + "table": "session_prompt_epoch_recovery" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_prompt_epoch_recovery" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": true, + "default": null, + "generated": null, + "name": "seq", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "correlation_id", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "mutation_epoch", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "consumed_seq", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "superseded_at", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "materialized_at", + "entityType": "columns", + "table": "session_steer" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "project_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_id", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "slug", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "path", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "title", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "share_url", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_additions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_deletions", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_files", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diffs", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "summary_diff_manifest", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "metadata", + "entityType": "columns", + "table": "session" + }, + { + "type": "real", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "cost", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_input", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_output", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_reasoning", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_read", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "tokens_cache_write", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "mutation_epoch", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "revert", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "permission", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "agent", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "model", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_compacting", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_archived", + "entityType": "columns", + "table": "session" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_suspended", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "preview", + "entityType": "columns", + "table": "session" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "command_id", + "entityType": "columns", + "table": "session_tool_request_resolution_command" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_hash", + "entityType": "columns", + "table": "session_tool_request_resolution_command" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_tool_request_resolution_command" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "receipt_id", + "entityType": "columns", + "table": "session_tool_request_resolution_command" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_resolution_id", + "entityType": "columns", + "table": "session_tool_request_resolution_command" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_tool_request_resolution_command" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "resolution_id", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "receipt_id", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "legacy_activity_id", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "assistant_message_id", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_prompt_epoch", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_window_id", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_effective_history_hash", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_request_hash", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_mutation_epoch", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expected_provider_state", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "decision", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_type", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "actor_id", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reason", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "risk_acknowledged", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "safe_end_message_id", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "safe_history_hash", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "safe_message_ids", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ambiguity_message_id", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "physical_message_high_water", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "successor_prompt_epoch", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "successor_window_id", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "successor_history_hash", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "successor_mutation_epoch", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_tool_request_resolution" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_world_state_baseline" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "prompt_epoch", + "entityType": "columns", + "table": "session_world_state_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "section_id", + "entityType": "columns", + "table": "session_world_state_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "snapshot", + "entityType": "columns", + "table": "session_world_state_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "fragment", + "entityType": "columns", + "table": "session_world_state_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "fragment_hash", + "entityType": "columns", + "table": "session_world_state_baseline" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "provenance", + "entityType": "columns", + "table": "session_world_state_baseline" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "session_world_state_baseline" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "admission_key", + "entityType": "columns", + "table": "task_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_hash", + "entityType": "columns", + "table": "task_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "run_id", + "entityType": "columns", + "table": "task_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_session_id", + "entityType": "columns", + "table": "task_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_message_id", + "entityType": "columns", + "table": "task_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "tool_call_id", + "entityType": "columns", + "table": "task_admission" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery_mode", + "entityType": "columns", + "table": "task_admission" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "task_admission" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "run_id", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_session_id", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "directory", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "payload", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "attempts", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "available_at", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "lease_owner", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "lease_expires_at", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_error", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_delivered", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'terminal'", + "generated": null, + "name": "event_kind", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "correlation_id", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "payload_hash", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_input_message_id", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "response_message_id", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "response_started_at", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_admitted", + "entityType": "columns", + "table": "task_notification_outbox" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "event_id", + "entityType": "columns", + "table": "task_run_event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "run_id", + "entityType": "columns", + "table": "task_run_event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "version", + "entityType": "columns", + "table": "task_run_event" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "type", + "entityType": "columns", + "table": "task_run_event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "from_state", + "entityType": "columns", + "table": "task_run_event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "to_state", + "entityType": "columns", + "table": "task_run_event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reason", + "entityType": "columns", + "table": "task_run_event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "data", + "entityType": "columns", + "table": "task_run_event" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "task_run_event" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "run_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "root_run_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_hash", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_session_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_message_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "tool_call_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "child_session_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "generation", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "delivery_mode", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "phase", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "state", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "reason", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "attempts", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "execution_owner", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "lease_expires_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "raw_result_message_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "structured_result_message_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "structured_output_receipt", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "output", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "error", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_settled", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "parent_run_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "continuation_of_run_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "1", + "generated": null, + "name": "depth", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'task_tool'", + "generated": null, + "name": "origin_kind", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "origin_key", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'foreground'", + "generated": null, + "name": "effective_delivery_mode", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "promoted_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'new'", + "generated": null, + "name": "session_mode", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'fresh'", + "generated": null, + "name": "context_mode", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "context_cutoff_message_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'write'", + "generated": null, + "name": "mutation_capability", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'legacy-unknown'", + "generated": null, + "name": "tool_capability_hash", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'shared'", + "generated": null, + "name": "workspace_mode", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'parent'", + "generated": null, + "name": "workspace_owner", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'live'", + "generated": null, + "name": "workspace_visibility", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'allow_live'", + "generated": null, + "name": "parent_dirty_policy", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_operation_key", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_revision", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "execution_spec", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "version", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'open'", + "generated": null, + "name": "control_state", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'legacy'", + "generated": null, + "name": "input_state", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "child_message_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "input_admission_started_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "child_input_materialized_hash", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "child_input_part_count", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "execution_started_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "finalizer_started_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "interrupt_requested_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "interrupt_reason", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "close_requested_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "close_reason", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "claim_generation", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "start_attempts", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "available_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "queue_reason", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'legacy'", + "generated": null, + "name": "workspace_preflight_state", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_preflight_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_repository_root", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_base_commit", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_parent_branch", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_target_branch", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_status_hash", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_preflight_error_code", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'none'", + "generated": null, + "name": "workspace_branch_state", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "workspace_branch_started_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree_directory", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree_branch", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": "'none'", + "generated": null, + "name": "worktree_state", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "worktree_started_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "pr_operation_key", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "pr_started_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "pr_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "goal_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "goal_tick_seq", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "goal_role", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "goal_ordinal", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_hash", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "usage", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": "0", + "generated": null, + "name": "progress_seq", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "integer", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "last_progress_at", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "finalizer_input_message_id", + "entityType": "columns", + "table": "task_run" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "run_id", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "attempt", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "child_session_id", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_token", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "claim_generation", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expected_version", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "source_message_id", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "request_message_id", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "response_message_id", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "response_message_json", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "task_structured_finalizer_response" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "run_id", + "entityType": "columns", + "table": "task_structured_output_evidence_part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "role", + "entityType": "columns", + "table": "task_structured_output_evidence_part" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "ordinal", + "entityType": "columns", + "table": "task_structured_output_evidence_part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "part_id", + "entityType": "columns", + "table": "task_structured_output_evidence_part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "message_id", + "entityType": "columns", + "table": "task_structured_output_evidence_part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "task_structured_output_evidence_part" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "part_json", + "entityType": "columns", + "table": "task_structured_output_evidence_part" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "run_id", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "child_session_id", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "owner_token", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "claim_generation", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "expected_version", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "terminal_state", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "attempts", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "contract_json", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "raw_result_message_id", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "raw_message_json", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "raw_parts_json", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_message_id", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_message_json", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "result_parts_json", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "output", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "structured_output_receipt", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "failure_code", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "created_at", + "entityType": "columns", + "table": "task_structured_output_evidence" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "content", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "status", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "priority", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "position", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "todo" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "todo" + }, + { + "type": "text", + "notNull": false, + "autoincrement": false, + "default": null, + "generated": null, + "name": "session_id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "id", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "secret", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "text", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "url", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_created", + "entityType": "columns", + "table": "session_share" + }, + { + "type": "integer", + "notNull": true, + "autoincrement": false, + "default": null, + "generated": null, + "name": "time_updated", + "entityType": "columns", + "table": "session_share" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_workspace_project_id_project_id_fk", + "entityType": "fks", + "table": "workspace" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_activity_objective_session_id_session_id_fk", + "entityType": "fks", + "table": "session_activity_objective" + }, + { + "columns": [ + "request_id" + ], + "tableTo": "session_activity_permission_request", + "columnsTo": [ + "request_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_activity_permission_effect_dispatch_request_id_session_activity_permission_request_request_id_fk", + "entityType": "fks", + "table": "session_activity_permission_effect_dispatch" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_activity_permission_effect_dispatch_session_id_session_id_fk", + "entityType": "fks", + "table": "session_activity_permission_effect_dispatch" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_activity_permission_effect_dispatch_project_id_project_id_fk", + "entityType": "fks", + "table": "session_activity_permission_effect_dispatch" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_activity_permission_request_session_id_session_id_fk", + "entityType": "fks", + "table": "session_activity_permission_request" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_activity_permission_request_project_id_project_id_fk", + "entityType": "fks", + "table": "session_activity_permission_request" + }, + { + "columns": [ + "parent_session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_facade_activity_parent_session_id_session_id_fk", + "entityType": "fks", + "table": "session_facade_activity" + }, + { + "columns": [ + "owner_session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_facade_activity_owner_session_id_session_id_fk", + "entityType": "fks", + "table": "session_facade_activity" + }, + { + "columns": [ + "job_id" + ], + "tableTo": "learning_job", + "columnsTo": [ + "job_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_learning_admission_outbox_job_id_learning_job_job_id_fk", + "entityType": "fks", + "table": "learning_admission_outbox" + }, + { + "columns": [ + "plan_id" + ], + "tableTo": "learning_governance_plan", + "columnsTo": [ + "plan_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_learning_governance_action_plan_id_learning_governance_plan_plan_id_fk", + "entityType": "fks", + "table": "learning_governance_action" + }, + { + "columns": [ + "plan_id" + ], + "tableTo": "learning_governance_plan", + "columnsTo": [ + "plan_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_learning_governance_compensation_plan_id_learning_governance_plan_plan_id_fk", + "entityType": "fks", + "table": "learning_governance_compensation" + }, + { + "columns": [ + "action_id" + ], + "tableTo": "learning_governance_action", + "columnsTo": [ + "action_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_learning_governance_compensation_action_id_learning_governance_action_action_id_fk", + "entityType": "fks", + "table": "learning_governance_compensation" + }, + { + "columns": [ + "job_id" + ], + "tableTo": "learning_job", + "columnsTo": [ + "job_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_learning_governance_plan_job_id_learning_job_job_id_fk", + "entityType": "fks", + "table": "learning_governance_plan" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_learning_job_project_id_project_id_fk", + "entityType": "fks", + "table": "learning_job" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_learning_job_session_id_session_id_fk", + "entityType": "fks", + "table": "learning_job" + }, + { + "columns": [ + "job_id" + ], + "tableTo": "learning_job", + "columnsTo": [ + "job_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_learning_reviewer_attempt_job_id_learning_job_job_id_fk", + "entityType": "fks", + "table": "learning_reviewer_attempt" + }, + { + "columns": [ + "security_namespace_id" + ], + "tableTo": "context_security_namespace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_released_knowledge_evaluation_security_namespace_id_context_security_namespace_id_fk", + "entityType": "fks", + "table": "released_knowledge_evaluation" + }, + { + "columns": [ + "security_namespace_id", + "project_scope_key" + ], + "tableTo": "context_project_scope_identity", + "columnsTo": [ + "security_namespace_id", + "project_scope_key" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_released_knowledge_evaluation_security_namespace_id_project_scope_key_context_project_scope_identity_security_namespace_id_project_scope_key_fk", + "entityType": "fks", + "table": "released_knowledge_evaluation" + }, + { + "columns": [ + "snapshot_id" + ], + "tableTo": "released_knowledge_snapshot", + "columnsTo": [ + "snapshot_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_released_knowledge_snapshot_document_snapshot_id_released_knowledge_snapshot_snapshot_id_fk", + "entityType": "fks", + "table": "released_knowledge_snapshot_document" + }, + { + "columns": [ + "security_namespace_id" + ], + "tableTo": "context_security_namespace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_released_knowledge_snapshot_head_security_namespace_id_context_security_namespace_id_fk", + "entityType": "fks", + "table": "released_knowledge_snapshot_head" + }, + { + "columns": [ + "security_namespace_id", + "project_scope_key" + ], + "tableTo": "context_project_scope_identity", + "columnsTo": [ + "security_namespace_id", + "project_scope_key" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_released_knowledge_snapshot_head_security_namespace_id_project_scope_key_context_project_scope_identity_security_namespace_id_project_scope_key_fk", + "entityType": "fks", + "table": "released_knowledge_snapshot_head" + }, + { + "columns": [ + "security_namespace_id", + "project_scope_key", + "snapshot_id" + ], + "tableTo": "released_knowledge_snapshot", + "columnsTo": [ + "security_namespace_id", + "project_scope_key", + "snapshot_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_released_knowledge_snapshot_head_security_namespace_id_project_scope_key_snapshot_id_released_knowledge_snapshot_security_namespace_id_project_scope_key_snapshot_id_fk", + "entityType": "fks", + "table": "released_knowledge_snapshot_head" + }, + { + "columns": [ + "security_namespace_id" + ], + "tableTo": "context_security_namespace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_released_knowledge_snapshot_security_namespace_id_context_security_namespace_id_fk", + "entityType": "fks", + "table": "released_knowledge_snapshot" + }, + { + "columns": [ + "security_namespace_id", + "project_scope_key" + ], + "tableTo": "context_project_scope_identity", + "columnsTo": [ + "security_namespace_id", + "project_scope_key" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_released_knowledge_snapshot_security_namespace_id_project_scope_key_context_project_scope_identity_security_namespace_id_project_scope_key_fk", + "entityType": "fks", + "table": "released_knowledge_snapshot" + }, + { + "columns": [ + "security_namespace_id", + "project_scope_key", + "parent_snapshot_id" + ], + "tableTo": "released_knowledge_snapshot", + "columnsTo": [ + "security_namespace_id", + "project_scope_key", + "snapshot_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_released_knowledge_snapshot_security_namespace_id_project_scope_key_parent_snapshot_id_released_knowledge_snapshot_security_namespace_id_project_scope_key_snapshot_id_fk", + "entityType": "fks", + "table": "released_knowledge_snapshot" + }, + { + "columns": [ + "security_namespace_id", + "project_scope_key", + "evaluation_id" + ], + "tableTo": "released_knowledge_evaluation", + "columnsTo": [ + "security_namespace_id", + "project_scope_key", + "evaluation_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_released_knowledge_snapshot_security_namespace_id_project_scope_key_evaluation_id_released_knowledge_evaluation_security_namespace_id_project_scope_key_evaluation_id_fk", + "entityType": "fks", + "table": "released_knowledge_snapshot" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_file_part_artifact_binding_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "file_part_artifact_binding" + }, + { + "columns": [ + "artifact_id" + ], + "tableTo": "file_part_artifact", + "columnsTo": [ + "artifact_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "RESTRICT", + "nameExplicit": false, + "name": "fk_file_part_artifact_binding_artifact_id_file_part_artifact_artifact_id_fk", + "entityType": "fks", + "table": "file_part_artifact_binding" + }, + { + "columns": [ + "artifact_id" + ], + "tableTo": "file_part_artifact", + "columnsTo": [ + "artifact_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_file_part_artifact_chunk_artifact_id_file_part_artifact_artifact_id_fk", + "entityType": "fks", + "table": "file_part_artifact_chunk" + }, + { + "columns": [ + "artifact_id" + ], + "tableTo": "file_part_artifact", + "columnsTo": [ + "artifact_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_file_part_artifact_discard_artifact_id_file_part_artifact_artifact_id_fk", + "entityType": "fks", + "table": "file_part_artifact_discard" + }, + { + "columns": [ + "artifact_id" + ], + "tableTo": "file_part_artifact", + "columnsTo": [ + "artifact_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_file_part_artifact_import_artifact_id_file_part_artifact_artifact_id_fk", + "entityType": "fks", + "table": "file_part_artifact_import" + }, + { + "columns": [ + "core_v2_receipt_id" + ], + "tableTo": "session_v2_provider_turn_receipt", + "columnsTo": [ + "receipt_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_session_v2_provider_parity_receipt_core_v2_receipt_id_session_v2_provider_turn_receipt_receipt_id_fk", + "entityType": "fks", + "table": "session_v2_provider_parity_receipt" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_v2_provider_turn_receipt_session_id_session_id_fk", + "entityType": "fks", + "table": "session_v2_provider_turn_receipt" + }, + { + "columns": [ + "owner_token" + ], + "tableTo": "session_provider_owner_lease", + "columnsTo": [ + "owner_token" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_session_v2_provider_turn_receipt_owner_token_session_provider_owner_lease_owner_token_fk", + "entityType": "fks", + "table": "session_v2_provider_turn_receipt" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_transfer_operation_session_id_session_id_fk", + "entityType": "fks", + "table": "session_transfer_operation" + }, + { + "columns": [ + "active_account_id" + ], + "tableTo": "account", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "SET NULL", + "nameExplicit": false, + "name": "fk_account_state_active_account_id_account_id_fk", + "entityType": "fks", + "table": "account_state" + }, + { + "columns": [ + "security_namespace_id" + ], + "tableTo": "context_security_namespace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_context_location_identity_alias_security_namespace_id_context_security_namespace_id_fk", + "entityType": "fks", + "table": "context_location_identity_alias" + }, + { + "columns": [ + "security_namespace_id" + ], + "tableTo": "context_security_namespace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_context_location_identity_security_namespace_id_context_security_namespace_id_fk", + "entityType": "fks", + "table": "context_location_identity" + }, + { + "columns": [ + "security_namespace_id" + ], + "tableTo": "context_security_namespace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_context_project_scope_identity_alias_security_namespace_id_context_security_namespace_id_fk", + "entityType": "fks", + "table": "context_project_scope_identity_alias" + }, + { + "columns": [ + "security_namespace_id" + ], + "tableTo": "context_security_namespace", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "NO ACTION", + "nameExplicit": false, + "name": "fk_context_project_scope_identity_security_namespace_id_context_security_namespace_id_fk", + "entityType": "fks", + "table": "context_project_scope_identity" + }, + { + "columns": [ + "artifact_id" + ], + "tableTo": "event_artifact", + "columnsTo": [ + "artifact_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_artifact_chunk_artifact_id_event_artifact_artifact_id_fk", + "entityType": "fks", + "table": "event_artifact_chunk" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_artifact_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event_artifact" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_compaction_receipt_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event_compaction_receipt" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_dedupe_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event_dedupe" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_snapshot_attempt_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event_snapshot_attempt" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_snapshot_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event_snapshot" + }, + { + "columns": [ + "aggregate_id" + ], + "tableTo": "event_sequence", + "columnsTo": [ + "aggregate_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_event_aggregate_id_event_sequence_aggregate_id_fk", + "entityType": "fks", + "table": "event" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_im_attachments_project_id_project_id_fk", + "entityType": "fks", + "table": "im_attachments" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_im_groups_project_id_project_id_fk", + "entityType": "fks", + "table": "im_groups" + }, + { + "columns": [ + "group_id" + ], + "tableTo": "im_groups", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_im_members_group_id_im_groups_id_fk", + "entityType": "fks", + "table": "im_members" + }, + { + "columns": [ + "group_id" + ], + "tableTo": "im_groups", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_im_messages_group_id_im_groups_id_fk", + "entityType": "fks", + "table": "im_messages" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_saved_epoch_project_id_project_id_fk", + "entityType": "fks", + "table": "permission_saved_epoch" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_permission_project_id_project_id_fk", + "entityType": "fks", + "table": "permission" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_project_directory_project_id_project_id_fk", + "entityType": "fks", + "table": "project_directory" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_message_session_id_session_id_fk", + "entityType": "fks", + "table": "message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_part_message_id_message_id_fk", + "entityType": "fks", + "table": "part" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_context_epoch_session_id_session_id_fk", + "entityType": "fks", + "table": "session_context_epoch" + }, + { + "columns": [ + "source_session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_fork_admission_source_session_id_session_id_fk", + "entityType": "fks", + "table": "session_fork_admission" + }, + { + "columns": [ + "source_session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_fork_intent_source_session_id_session_id_fk", + "entityType": "fks", + "table": "session_fork_intent" + }, + { + "columns": [ + "target_session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_fork_intent_target_session_id_session_id_fk", + "entityType": "fks", + "table": "session_fork_intent" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_history_state_session_id_session_id_fk", + "entityType": "fks", + "table": "session_history_state" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_input_session_id_session_id_fk", + "entityType": "fks", + "table": "session_input" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_intent_session_id_session_id_fk", + "entityType": "fks", + "table": "session_intent" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_message" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_prompt_epoch_message_session_id_session_id_fk", + "entityType": "fks", + "table": "session_prompt_epoch_message" + }, + { + "columns": [ + "message_id" + ], + "tableTo": "message", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_prompt_epoch_message_message_id_message_id_fk", + "entityType": "fks", + "table": "session_prompt_epoch_message" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_steer_session_id_session_id_fk", + "entityType": "fks", + "table": "session_steer" + }, + { + "columns": [ + "project_id" + ], + "tableTo": "project", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_project_id_project_id_fk", + "entityType": "fks", + "table": "session" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_world_state_baseline_session_id_session_id_fk", + "entityType": "fks", + "table": "session_world_state_baseline" + }, + { + "columns": [ + "run_id" + ], + "tableTo": "task_run", + "columnsTo": [ + "run_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_admission_run_id_task_run_run_id_fk", + "entityType": "fks", + "table": "task_admission" + }, + { + "columns": [ + "parent_session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_admission_parent_session_id_session_id_fk", + "entityType": "fks", + "table": "task_admission" + }, + { + "columns": [ + "run_id" + ], + "tableTo": "task_run", + "columnsTo": [ + "run_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_notification_outbox_run_id_task_run_run_id_fk", + "entityType": "fks", + "table": "task_notification_outbox" + }, + { + "columns": [ + "parent_session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_notification_outbox_parent_session_id_session_id_fk", + "entityType": "fks", + "table": "task_notification_outbox" + }, + { + "columns": [ + "run_id" + ], + "tableTo": "task_run", + "columnsTo": [ + "run_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_run_event_run_id_task_run_run_id_fk", + "entityType": "fks", + "table": "task_run_event" + }, + { + "columns": [ + "parent_session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_run_parent_session_id_session_id_fk", + "entityType": "fks", + "table": "task_run" + }, + { + "columns": [ + "parent_run_id" + ], + "tableTo": "task_run", + "columnsTo": [ + "run_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_run_parent_run_id_task_run_run_id_fk", + "entityType": "fks", + "table": "task_run" + }, + { + "columns": [ + "continuation_of_run_id" + ], + "tableTo": "task_run", + "columnsTo": [ + "run_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_run_continuation_of_run_id_task_run_run_id_fk", + "entityType": "fks", + "table": "task_run" + }, + { + "columns": [ + "run_id" + ], + "tableTo": "task_run", + "columnsTo": [ + "run_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_structured_finalizer_response_run_id_task_run_run_id_fk", + "entityType": "fks", + "table": "task_structured_finalizer_response" + }, + { + "columns": [ + "run_id" + ], + "tableTo": "task_structured_output_evidence", + "columnsTo": [ + "run_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_structured_output_evidence_part_run_id_task_structured_output_evidence_run_id_fk", + "entityType": "fks", + "table": "task_structured_output_evidence_part" + }, + { + "columns": [ + "run_id" + ], + "tableTo": "task_run", + "columnsTo": [ + "run_id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_task_structured_output_evidence_run_id_task_run_run_id_fk", + "entityType": "fks", + "table": "task_structured_output_evidence" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_todo_session_id_session_id_fk", + "entityType": "fks", + "table": "todo" + }, + { + "columns": [ + "session_id" + ], + "tableTo": "session", + "columnsTo": [ + "id" + ], + "onUpdate": "NO ACTION", + "onDelete": "CASCADE", + "nameExplicit": false, + "name": "fk_session_share_session_id_session_id_fk", + "entityType": "fks", + "table": "session_share" + }, + { + "columns": [ + "activity_kind", + "activity_id", + "receipt_id" + ], + "nameExplicit": false, + "name": "session_activity_effect_receipt_pk", + "entityType": "pks", + "table": "session_activity_effect_receipt" + }, + { + "columns": [ + "activity_kind", + "activity_id", + "evidence_fingerprint" + ], + "nameExplicit": false, + "name": "session_activity_evidence_pk", + "entityType": "pks", + "table": "session_activity_evidence" + }, + { + "columns": [ + "activity_kind", + "activity_id" + ], + "nameExplicit": false, + "name": "session_activity_objective_pk", + "entityType": "pks", + "table": "session_activity_objective" + }, + { + "columns": [ + "activity_kind", + "activity_id", + "revision" + ], + "nameExplicit": false, + "name": "session_activity_progress_observation_pk", + "entityType": "pks", + "table": "session_activity_progress_observation" + }, + { + "columns": [ + "snapshot_id", + "source_store", + "doc_id" + ], + "nameExplicit": false, + "name": "released_knowledge_snapshot_document_pk", + "entityType": "pks", + "table": "released_knowledge_snapshot_document" + }, + { + "columns": [ + "security_namespace_id", + "project_scope_key" + ], + "nameExplicit": false, + "name": "released_knowledge_snapshot_head_pk", + "entityType": "pks", + "table": "released_knowledge_snapshot_head" + }, + { + "columns": [ + "artifact_id", + "chunk_index" + ], + "nameExplicit": false, + "name": "file_part_artifact_chunk_pk", + "entityType": "pks", + "table": "file_part_artifact_chunk" + }, + { + "columns": [ + "campaign_id", + "case_name" + ], + "nameExplicit": false, + "name": "session_v2_provider_parity_baseline_pk", + "entityType": "pks", + "table": "session_v2_provider_parity_baseline" + }, + { + "columns": [ + "campaign_id", + "case_name" + ], + "nameExplicit": false, + "name": "session_v2_provider_parity_receipt_pk", + "entityType": "pks", + "table": "session_v2_provider_parity_receipt" + }, + { + "columns": [ + "email", + "url" + ], + "nameExplicit": false, + "name": "control_account_pk", + "entityType": "pks", + "table": "control_account" + }, + { + "columns": [ + "security_namespace_id", + "old_canonical_root" + ], + "nameExplicit": false, + "name": "context_location_identity_alias_pk", + "entityType": "pks", + "table": "context_location_identity_alias" + }, + { + "columns": [ + "security_namespace_id", + "location_key" + ], + "nameExplicit": false, + "name": "context_location_identity_pk", + "entityType": "pks", + "table": "context_location_identity" + }, + { + "columns": [ + "index_space_id", + "projection_kind" + ], + "nameExplicit": false, + "name": "location_index_coordination_pk", + "entityType": "pks", + "table": "location_index_coordination" + }, + { + "columns": [ + "security_namespace_id", + "old_project_identity_hash" + ], + "nameExplicit": false, + "name": "context_project_scope_identity_alias_pk", + "entityType": "pks", + "table": "context_project_scope_identity_alias" + }, + { + "columns": [ + "security_namespace_id", + "project_scope_key" + ], + "nameExplicit": false, + "name": "context_project_scope_identity_pk", + "entityType": "pks", + "table": "context_project_scope_identity" + }, + { + "columns": [ + "artifact_id", + "chunk_index" + ], + "nameExplicit": false, + "name": "event_artifact_chunk_pk", + "entityType": "pks", + "table": "event_artifact_chunk" + }, + { + "columns": [ + "row_hash", + "chunk_index" + ], + "nameExplicit": false, + "name": "event_snapshot_chunk_pk", + "entityType": "pks", + "table": "event_snapshot_chunk" + }, + { + "columns": [ + "snapshot_id", + "row_index" + ], + "nameExplicit": false, + "name": "event_snapshot_row_pk", + "entityType": "pks", + "table": "event_snapshot_row" + }, + { + "columns": [ + "workspace_id", + "remote_fingerprint" + ], + "nameExplicit": false, + "name": "workspace_sync_cursor_pk", + "entityType": "pks", + "table": "workspace_sync_cursor" + }, + { + "columns": [ + "index_space_id", + "projection_kind", + "path" + ], + "nameExplicit": false, + "name": "location_projection_dirty_path_pk", + "entityType": "pks", + "table": "location_projection_dirty_path" + }, + { + "columns": [ + "index_space_id", + "projection_kind" + ], + "nameExplicit": false, + "name": "location_projection_registration_pk", + "entityType": "pks", + "table": "location_projection_registration" + }, + { + "columns": [ + "project_id", + "directory" + ], + "nameExplicit": false, + "name": "project_directory_pk", + "entityType": "pks", + "table": "project_directory" + }, + { + "columns": [ + "session_id", + "prompt_epoch", + "ordinal" + ], + "nameExplicit": false, + "name": "session_prompt_epoch_message_pk", + "entityType": "pks", + "table": "session_prompt_epoch_message" + }, + { + "columns": [ + "session_id", + "prompt_epoch" + ], + "nameExplicit": false, + "name": "session_prompt_epoch_recovery_pk", + "entityType": "pks", + "table": "session_prompt_epoch_recovery" + }, + { + "columns": [ + "session_id", + "prompt_epoch", + "section_id" + ], + "nameExplicit": false, + "name": "session_world_state_baseline_pk", + "entityType": "pks", + "table": "session_world_state_baseline" + }, + { + "columns": [ + "run_id", + "attempt" + ], + "nameExplicit": false, + "name": "task_structured_finalizer_response_pk", + "entityType": "pks", + "table": "task_structured_finalizer_response" + }, + { + "columns": [ + "run_id", + "role", + "part_id" + ], + "nameExplicit": false, + "name": "task_structured_output_evidence_part_pk", + "entityType": "pks", + "table": "task_structured_output_evidence_part" + }, + { + "columns": [ + "session_id", + "position" + ], + "nameExplicit": false, + "name": "todo_pk", + "entityType": "pks", + "table": "todo" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "workspace_pk", + "table": "workspace", + "entityType": "pks" + }, + { + "columns": [ + "name" + ], + "nameExplicit": false, + "name": "data_migration_pk", + "table": "data_migration", + "entityType": "pks" + }, + { + "columns": [ + "decision_id" + ], + "nameExplicit": false, + "name": "session_activity_permission_decision_pk", + "table": "session_activity_permission_decision", + "entityType": "pks" + }, + { + "columns": [ + "receipt_id" + ], + "nameExplicit": false, + "name": "session_activity_permission_effect_dispatch_pk", + "table": "session_activity_permission_effect_dispatch", + "entityType": "pks" + }, + { + "columns": [ + "request_id" + ], + "nameExplicit": false, + "name": "session_activity_permission_once_consumption_pk", + "table": "session_activity_permission_once_consumption", + "entityType": "pks" + }, + { + "columns": [ + "owner_id" + ], + "nameExplicit": false, + "name": "session_activity_permission_owner_lease_pk", + "table": "session_activity_permission_owner_lease", + "entityType": "pks" + }, + { + "columns": [ + "request_id" + ], + "nameExplicit": false, + "name": "session_activity_permission_request_pk", + "table": "session_activity_permission_request", + "entityType": "pks" + }, + { + "columns": [ + "activity_id" + ], + "nameExplicit": false, + "name": "session_facade_activity_pk", + "table": "session_facade_activity", + "entityType": "pks" + }, + { + "columns": [ + "intent_id" + ], + "nameExplicit": false, + "name": "learning_admission_outbox_pk", + "table": "learning_admission_outbox", + "entityType": "pks" + }, + { + "columns": [ + "action_id" + ], + "nameExplicit": false, + "name": "learning_governance_action_pk", + "table": "learning_governance_action", + "entityType": "pks" + }, + { + "columns": [ + "compensation_id" + ], + "nameExplicit": false, + "name": "learning_governance_compensation_pk", + "table": "learning_governance_compensation", + "entityType": "pks" + }, + { + "columns": [ + "plan_id" + ], + "nameExplicit": false, + "name": "learning_governance_plan_pk", + "table": "learning_governance_plan", + "entityType": "pks" + }, + { + "columns": [ + "job_id" + ], + "nameExplicit": false, + "name": "learning_job_pk", + "table": "learning_job", + "entityType": "pks" + }, + { + "columns": [ + "receipt_id" + ], + "nameExplicit": false, + "name": "learning_lifecycle_trigger_receipt_pk", + "table": "learning_lifecycle_trigger_receipt", + "entityType": "pks" + }, + { + "columns": [ + "attempt_id" + ], + "nameExplicit": false, + "name": "learning_reviewer_attempt_pk", + "table": "learning_reviewer_attempt", + "entityType": "pks" + }, + { + "columns": [ + "evaluation_id" + ], + "nameExplicit": false, + "name": "released_knowledge_evaluation_pk", + "table": "released_knowledge_evaluation", + "entityType": "pks" + }, + { + "columns": [ + "snapshot_id" + ], + "nameExplicit": false, + "name": "released_knowledge_snapshot_pk", + "table": "released_knowledge_snapshot", + "entityType": "pks" + }, + { + "columns": [ + "event_id" + ], + "nameExplicit": false, + "name": "file_part_artifact_binding_pk", + "table": "file_part_artifact_binding", + "entityType": "pks" + }, + { + "columns": [ + "event_id" + ], + "nameExplicit": false, + "name": "file_part_artifact_discard_pk", + "table": "file_part_artifact_discard", + "entityType": "pks" + }, + { + "columns": [ + "event_id" + ], + "nameExplicit": false, + "name": "file_part_artifact_import_pk", + "table": "file_part_artifact_import", + "entityType": "pks" + }, + { + "columns": [ + "artifact_id" + ], + "nameExplicit": false, + "name": "file_part_artifact_pk", + "table": "file_part_artifact", + "entityType": "pks" + }, + { + "columns": [ + "receipt_id" + ], + "nameExplicit": false, + "name": "session_v2_provider_turn_receipt_pk", + "table": "session_v2_provider_turn_receipt", + "entityType": "pks" + }, + { + "columns": [ + "transfer_id" + ], + "nameExplicit": false, + "name": "session_transfer_operation_pk", + "table": "session_transfer_operation", + "entityType": "pks" + }, + { + "columns": [ + "transfer_id" + ], + "nameExplicit": false, + "name": "session_transfer_target_receipt_pk", + "table": "session_transfer_target_receipt", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_state_pk", + "table": "account_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "account_pk", + "table": "account", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "context_security_namespace_pk", + "table": "context_security_namespace", + "entityType": "pks" + }, + { + "columns": [ + "artifact_id" + ], + "nameExplicit": false, + "name": "event_artifact_pk", + "table": "event_artifact", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_compaction_receipt_pk", + "table": "event_compaction_receipt", + "entityType": "pks" + }, + { + "columns": [ + "aggregate_id" + ], + "nameExplicit": false, + "name": "event_sequence_pk", + "table": "event_sequence", + "entityType": "pks" + }, + { + "columns": [ + "snapshot_id" + ], + "nameExplicit": false, + "name": "event_snapshot_attempt_pk", + "table": "event_snapshot_attempt", + "entityType": "pks" + }, + { + "columns": [ + "snapshot_id" + ], + "nameExplicit": false, + "name": "event_snapshot_pk", + "table": "event_snapshot", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_sync_backfill_pk", + "table": "event_sync_backfill", + "entityType": "pks" + }, + { + "columns": [ + "sync_seq" + ], + "nameExplicit": false, + "name": "event_sync_index_pk", + "table": "event_sync_index", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_sync_sequence_pk", + "table": "event_sync_sequence", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "event_pk", + "table": "event", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "im_attachments_pk", + "table": "im_attachments", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "im_groups_pk", + "table": "im_groups", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "im_messages_pk", + "table": "im_messages", + "entityType": "pks" + }, + { + "columns": [ + "event_seq" + ], + "nameExplicit": false, + "name": "location_change_event_pk", + "table": "location_change_event", + "entityType": "pks" + }, + { + "columns": [ + "project_id" + ], + "nameExplicit": false, + "name": "permission_saved_epoch_pk", + "table": "permission_saved_epoch", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "permission_pk", + "table": "permission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "project_pk", + "table": "project", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "message_pk", + "table": "message", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "part_pk", + "table": "part", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_context_epoch_pk", + "table": "session_context_epoch", + "entityType": "pks" + }, + { + "columns": [ + "intent_id" + ], + "nameExplicit": false, + "name": "session_fork_admission_pk", + "table": "session_fork_admission", + "entityType": "pks" + }, + { + "columns": [ + "intent_id" + ], + "nameExplicit": false, + "name": "session_fork_intent_pk", + "table": "session_fork_intent", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_history_state_pk", + "table": "session_history_state", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_input_pk", + "table": "session_input", + "entityType": "pks" + }, + { + "columns": [ + "intent_id" + ], + "nameExplicit": false, + "name": "session_intent_pk", + "table": "session_intent", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_message_pk", + "table": "session_message", + "entityType": "pks" + }, + { + "columns": [ + "part_id" + ], + "nameExplicit": false, + "name": "session_part_integrity_quarantine_pk", + "table": "session_part_integrity_quarantine", + "entityType": "pks" + }, + { + "columns": [ + "seq" + ], + "nameExplicit": false, + "name": "session_steer_pk", + "table": "session_steer", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_pk", + "table": "session", + "entityType": "pks" + }, + { + "columns": [ + "command_id" + ], + "nameExplicit": false, + "name": "session_tool_request_resolution_command_pk", + "table": "session_tool_request_resolution_command", + "entityType": "pks" + }, + { + "columns": [ + "resolution_id" + ], + "nameExplicit": false, + "name": "session_tool_request_resolution_pk", + "table": "session_tool_request_resolution", + "entityType": "pks" + }, + { + "columns": [ + "admission_key" + ], + "nameExplicit": false, + "name": "task_admission_pk", + "table": "task_admission", + "entityType": "pks" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "task_notification_outbox_pk", + "table": "task_notification_outbox", + "entityType": "pks" + }, + { + "columns": [ + "event_id" + ], + "nameExplicit": false, + "name": "task_run_event_pk", + "table": "task_run_event", + "entityType": "pks" + }, + { + "columns": [ + "run_id" + ], + "nameExplicit": false, + "name": "task_run_pk", + "table": "task_run", + "entityType": "pks" + }, + { + "columns": [ + "run_id" + ], + "nameExplicit": false, + "name": "task_structured_output_evidence_pk", + "table": "task_structured_output_evidence", + "entityType": "pks" + }, + { + "columns": [ + "session_id" + ], + "nameExplicit": false, + "name": "session_share_pk", + "table": "session_share", + "entityType": "pks" + }, + { + "columns": [ + { + "value": "activity_kind", + "isExpression": false + }, + { + "value": "activity_id", + "isExpression": false + }, + { + "value": "first_observation_revision", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_activity_effect_receipt_activity_idx", + "entityType": "indexes", + "table": "session_activity_effect_receipt" + }, + { + "columns": [ + { + "value": "activity_kind", + "isExpression": false + }, + { + "value": "activity_id", + "isExpression": false + }, + { + "value": "first_observation_revision", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_activity_evidence_activity_idx", + "entityType": "indexes", + "table": "session_activity_evidence" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "state", + "isExpression": false + }, + { + "value": "updated_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_activity_objective_session_idx", + "entityType": "indexes", + "table": "session_activity_objective" + }, + { + "columns": [ + { + "value": "request_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_activity_permission_decision_request_idx", + "entityType": "indexes", + "table": "session_activity_permission_decision" + }, + { + "columns": [ + { + "value": "idempotency_key", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_activity_permission_decision_idempotency_idx", + "entityType": "indexes", + "table": "session_activity_permission_decision" + }, + { + "columns": [ + { + "value": "request_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_activity_permission_effect_dispatch_request_idx", + "entityType": "indexes", + "table": "session_activity_permission_effect_dispatch" + }, + { + "columns": [ + { + "value": "idempotency_key", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_activity_permission_effect_dispatch_idempotency_idx", + "entityType": "indexes", + "table": "session_activity_permission_effect_dispatch" + }, + { + "columns": [ + { + "value": "activity_kind", + "isExpression": false + }, + { + "value": "activity_id", + "isExpression": false + }, + { + "value": "state", + "isExpression": false + }, + { + "value": "started_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_activity_permission_effect_dispatch_activity_idx", + "entityType": "indexes", + "table": "session_activity_permission_effect_dispatch" + }, + { + "columns": [ + { + "value": "owner_id", + "isExpression": false + }, + { + "value": "state", + "isExpression": false + }, + { + "value": "started_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_activity_permission_effect_dispatch_owner_idx", + "entityType": "indexes", + "table": "session_activity_permission_effect_dispatch" + }, + { + "columns": [ + { + "value": "idempotency_key", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_activity_permission_once_consumption_idempotency_idx", + "entityType": "indexes", + "table": "session_activity_permission_once_consumption" + }, + { + "columns": [ + { + "value": "lease_expires_at", + "isExpression": false + }, + { + "value": "owner_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_activity_permission_owner_lease_expiry_idx", + "entityType": "indexes", + "table": "session_activity_permission_owner_lease" + }, + { + "columns": [ + { + "value": "idempotency_key", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_activity_permission_request_idempotency_idx", + "entityType": "indexes", + "table": "session_activity_permission_request" + }, + { + "columns": [ + { + "value": "activity_kind", + "isExpression": false + }, + { + "value": "activity_id", + "isExpression": false + } + ], + "isUnique": true, + "where": "\"session_activity_permission_request\".\"state\" = 'pending' AND \"session_activity_permission_request\".\"request_kind\" = 'no_progress'", + "origin": "manual", + "name": "session_activity_permission_request_pending_no_progress_idx", + "entityType": "indexes", + "table": "session_activity_permission_request" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "state", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_activity_permission_request_pending_idx", + "entityType": "indexes", + "table": "session_activity_permission_request" + }, + { + "columns": [ + { + "value": "idempotency_key", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_activity_progress_observation_idempotency_idx", + "entityType": "indexes", + "table": "session_activity_progress_observation" + }, + { + "columns": [ + { + "value": "activity_kind", + "isExpression": false + }, + { + "value": "activity_id", + "isExpression": false + }, + { + "value": "observed_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_activity_progress_observation_latest_idx", + "entityType": "indexes", + "table": "session_activity_progress_observation" + }, + { + "columns": [ + { + "value": "parent_session_id", + "isExpression": false + }, + { + "value": "subkind", + "isExpression": false + } + ], + "isUnique": true, + "where": "\"session_facade_activity\".\"state\" = 'active'", + "origin": "manual", + "name": "session_facade_activity_active_idx", + "entityType": "indexes", + "table": "session_facade_activity" + }, + { + "columns": [ + { + "value": "parent_session_id", + "isExpression": false + }, + { + "value": "state", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_facade_activity_parent_idx", + "entityType": "indexes", + "table": "session_facade_activity" + }, + { + "columns": [ + { + "value": "dedupe_key", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "learning_admission_outbox_dedupe_idx", + "entityType": "indexes", + "table": "learning_admission_outbox" + }, + { + "columns": [ + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": "\"learning_admission_outbox\".\"state\" = 'pending'", + "origin": "manual", + "name": "learning_admission_outbox_pending_idx", + "entityType": "indexes", + "table": "learning_admission_outbox" + }, + { + "columns": [ + { + "value": "plan_id", + "isExpression": false + }, + { + "value": "sequence", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "learning_governance_action_plan_sequence_idx", + "entityType": "indexes", + "table": "learning_governance_action" + }, + { + "columns": [ + { + "value": "plan_id", + "isExpression": false + }, + { + "value": "candidate_id", + "isExpression": false + }, + { + "value": "kind", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "learning_governance_action_plan_candidate_kind_idx", + "entityType": "indexes", + "table": "learning_governance_action" + }, + { + "columns": [ + { + "value": "plan_id", + "isExpression": false + }, + { + "value": "state", + "isExpression": false + }, + { + "value": "sequence", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "learning_governance_action_claim_idx", + "entityType": "indexes", + "table": "learning_governance_action" + }, + { + "columns": [ + { + "value": "action_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "learning_governance_compensation_action_idx", + "entityType": "indexes", + "table": "learning_governance_compensation" + }, + { + "columns": [ + { + "value": "plan_id", + "isExpression": false + }, + { + "value": "sequence", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "learning_governance_compensation_plan_sequence_idx", + "entityType": "indexes", + "table": "learning_governance_compensation" + }, + { + "columns": [ + { + "value": "state", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "learning_governance_compensation_claim_idx", + "entityType": "indexes", + "table": "learning_governance_compensation" + }, + { + "columns": [ + { + "value": "job_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "learning_governance_plan_job_idx", + "entityType": "indexes", + "table": "learning_governance_plan" + }, + { + "columns": [ + { + "value": "state", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "learning_governance_plan_state_idx", + "entityType": "indexes", + "table": "learning_governance_plan" + }, + { + "columns": [ + { + "value": "dedupe_key", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "learning_job_dedupe_idx", + "entityType": "indexes", + "table": "learning_job" + }, + { + "columns": [ + { + "value": "state", + "isExpression": false + }, + { + "value": "next_attempt_at", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "learning_job_due_idx", + "entityType": "indexes", + "table": "learning_job" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "learning_job_project_created_idx", + "entityType": "indexes", + "table": "learning_job" + }, + { + "columns": [ + { + "value": "owner", + "isExpression": false + }, + { + "value": "lease_expires_at", + "isExpression": false + } + ], + "isUnique": false, + "where": "\"learning_job\".\"owner\" IS NOT NULL", + "origin": "manual", + "name": "learning_job_owner_lease_idx", + "entityType": "indexes", + "table": "learning_job" + }, + { + "columns": [ + { + "value": "trigger", + "isExpression": false + }, + { + "value": "session_id", + "isExpression": false + }, + { + "value": "run_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "learning_lifecycle_trigger_identity_idx", + "entityType": "indexes", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "columns": [ + { + "value": "created_at", + "isExpression": false + }, + { + "value": "receipt_id", + "isExpression": false + } + ], + "isUnique": false, + "where": "\"learning_lifecycle_trigger_receipt\".\"state\" = 'prepared'", + "origin": "manual", + "name": "learning_lifecycle_trigger_pending_idx", + "entityType": "indexes", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "columns": [ + { + "value": "job_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "learning_reviewer_attempt_job_idx", + "entityType": "indexes", + "table": "learning_reviewer_attempt" + }, + { + "columns": [ + { + "value": "review_session_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "learning_reviewer_attempt_session_idx", + "entityType": "indexes", + "table": "learning_reviewer_attempt" + }, + { + "columns": [ + { + "value": "state", + "isExpression": false + }, + { + "value": "updated_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "learning_reviewer_attempt_state_idx", + "entityType": "indexes", + "table": "learning_reviewer_attempt" + }, + { + "columns": [ + { + "value": "security_namespace_id", + "isExpression": false + }, + { + "value": "project_scope_key", + "isExpression": false + }, + { + "value": "evaluation_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "released_knowledge_evaluation_scope_identity_idx", + "entityType": "indexes", + "table": "released_knowledge_evaluation" + }, + { + "columns": [ + { + "value": "security_namespace_id", + "isExpression": false + }, + { + "value": "project_scope_key", + "isExpression": false + }, + { + "value": "matrix_hash", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "released_knowledge_evaluation_matrix_idx", + "entityType": "indexes", + "table": "released_knowledge_evaluation" + }, + { + "columns": [ + { + "value": "snapshot_id", + "isExpression": false + }, + { + "value": "ordinal", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "released_knowledge_snapshot_document_ordinal_idx", + "entityType": "indexes", + "table": "released_knowledge_snapshot_document" + }, + { + "columns": [ + { + "value": "security_namespace_id", + "isExpression": false + }, + { + "value": "project_scope_key", + "isExpression": false + }, + { + "value": "snapshot_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "released_knowledge_snapshot_scope_identity_idx", + "entityType": "indexes", + "table": "released_knowledge_snapshot" + }, + { + "columns": [ + { + "value": "parent_snapshot_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "released_knowledge_snapshot_parent_idx", + "entityType": "indexes", + "table": "released_knowledge_snapshot" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "file_part_artifact_binding_aggregate_seq_idx", + "entityType": "indexes", + "table": "file_part_artifact_binding" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "part_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "file_part_artifact_binding_part_idx", + "entityType": "indexes", + "table": "file_part_artifact_binding" + }, + { + "columns": [ + { + "value": "artifact_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "file_part_artifact_binding_artifact_idx", + "entityType": "indexes", + "table": "file_part_artifact_binding" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "file_part_artifact_discard_aggregate_seq_idx", + "entityType": "indexes", + "table": "file_part_artifact_discard" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "file_part_artifact_import_aggregate_seq_idx", + "entityType": "indexes", + "table": "file_part_artifact_import" + }, + { + "columns": [ + { + "value": "artifact_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "file_part_artifact_import_artifact_idx", + "entityType": "indexes", + "table": "file_part_artifact_import" + }, + { + "columns": [ + { + "value": "receipt_hash", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_v2_provider_parity_baseline_hash_idx", + "entityType": "indexes", + "table": "session_v2_provider_parity_baseline" + }, + { + "columns": [ + { + "value": "campaign_id", + "isExpression": false + }, + { + "value": "state", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_v2_provider_parity_baseline_campaign_idx", + "entityType": "indexes", + "table": "session_v2_provider_parity_baseline" + }, + { + "columns": [ + { + "value": "receipt_hash", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_v2_provider_parity_receipt_hash_idx", + "entityType": "indexes", + "table": "session_v2_provider_parity_receipt" + }, + { + "columns": [ + { + "value": "campaign_id", + "isExpression": false + }, + { + "value": "verified", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_v2_provider_parity_receipt_campaign_idx", + "entityType": "indexes", + "table": "session_v2_provider_parity_receipt" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "request_ordinal", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_v2_provider_turn_receipt_ordinal_idx", + "entityType": "indexes", + "table": "session_v2_provider_turn_receipt" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "user_message_id", + "isExpression": false + }, + { + "value": "history_prompt_epoch", + "isExpression": false + }, + { + "value": "request_input_hash", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_v2_provider_turn_receipt_input_idx", + "entityType": "indexes", + "table": "session_v2_provider_turn_receipt" + }, + { + "columns": [ + { + "value": "owner_token", + "isExpression": false + }, + { + "value": "state", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_v2_provider_turn_receipt_owner_state_idx", + "entityType": "indexes", + "table": "session_v2_provider_turn_receipt" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "request_hash", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_transfer_operation_session_request_idx", + "entityType": "indexes", + "table": "session_transfer_operation" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": true, + "where": "\"session_transfer_operation\".\"state\" NOT IN ('target_activated', 'aborted')", + "origin": "manual", + "name": "session_transfer_operation_active_idx", + "entityType": "indexes", + "table": "session_transfer_operation" + }, + { + "columns": [ + { + "value": "security_namespace_id", + "isExpression": false + }, + { + "value": "canonical_root", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "context_location_identity_root_idx", + "entityType": "indexes", + "table": "context_location_identity" + }, + { + "columns": [ + { + "value": "security_namespace_id", + "isExpression": false + }, + { + "value": "location_key", + "isExpression": false + }, + { + "value": "projection_kind", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "location_index_coordination_location_idx", + "entityType": "indexes", + "table": "location_index_coordination" + }, + { + "columns": [ + { + "value": "security_namespace_id", + "isExpression": false + }, + { + "value": "project_identity_hash", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "context_project_scope_identity_value_idx", + "entityType": "indexes", + "table": "context_project_scope_identity" + }, + { + "columns": [ + { + "value": "kind", + "isExpression": false + }, + { + "value": "binding_hash", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "context_security_namespace_binding_idx", + "entityType": "indexes", + "table": "context_security_namespace" + }, + { + "columns": [ + { + "value": "event_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_artifact_event_idx", + "entityType": "indexes", + "table": "event_artifact" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_artifact_aggregate_seq_idx", + "entityType": "indexes", + "table": "event_artifact" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_dedupe_aggregate_seq_idx", + "entityType": "indexes", + "table": "event_dedupe" + }, + { + "columns": [ + { + "value": "event_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_dedupe_event_idx", + "entityType": "indexes", + "table": "event_dedupe" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_snapshot_attempt_aggregate_idx", + "entityType": "indexes", + "table": "event_snapshot_attempt" + }, + { + "columns": [ + { + "value": "snapshot_id", + "isExpression": false + }, + { + "value": "table_name", + "isExpression": false + }, + { + "value": "row_key", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_snapshot_row_identity_idx", + "entityType": "indexes", + "table": "event_snapshot_row" + }, + { + "columns": [ + { + "value": "row_hash", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_snapshot_row_hash_idx", + "entityType": "indexes", + "table": "event_snapshot_row" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_snapshot_row_aggregate_idx", + "entityType": "indexes", + "table": "event_snapshot_row" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "through_seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_snapshot_aggregate_seq_idx", + "entityType": "indexes", + "table": "event_snapshot" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_snapshot_aggregate_created_idx", + "entityType": "indexes", + "table": "event_snapshot" + }, + { + "columns": [ + { + "value": "sync_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_snapshot_sync_seq_idx", + "entityType": "indexes", + "table": "event_snapshot" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_sync_index_aggregate_seq_idx", + "entityType": "indexes", + "table": "event_sync_index" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "event_aggregate_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "aggregate_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "event_aggregate_type_seq_idx", + "entityType": "indexes", + "table": "event" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "idx_im_attachments_workspace", + "entityType": "indexes", + "table": "im_attachments" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "idx_im_attachments_message", + "entityType": "indexes", + "table": "im_attachments" + }, + { + "columns": [ + { + "value": "group_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "idx_im_attachments_group", + "entityType": "indexes", + "table": "im_attachments" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "im_groups_workspace_idx", + "entityType": "indexes", + "table": "im_groups" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "im_groups_project_idx", + "entityType": "indexes", + "table": "im_groups" + }, + { + "columns": [ + { + "value": "group_id", + "isExpression": false + }, + { + "value": "member_id", + "isExpression": false + }, + { + "value": "member_type", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "im_members_unique_idx", + "entityType": "indexes", + "table": "im_members" + }, + { + "columns": [ + { + "value": "member_id", + "isExpression": false + }, + { + "value": "group_id", + "isExpression": false + }, + { + "value": "last_read_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "idx_im_members_unread", + "entityType": "indexes", + "table": "im_members" + }, + { + "columns": [ + { + "value": "group_id", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "idx_im_messages_active", + "entityType": "indexes", + "table": "im_messages" + }, + { + "columns": [ + { + "value": "group_id", + "isExpression": false + }, + { + "value": "reply_to_id", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "idx_im_messages_thread", + "entityType": "indexes", + "table": "im_messages" + }, + { + "columns": [ + { + "value": "event_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "idx_im_messages_event", + "entityType": "indexes", + "table": "im_messages" + }, + { + "columns": [ + { + "value": "index_space_id", + "isExpression": false + }, + { + "value": "event_seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "location_change_event_space_seq_idx", + "entityType": "indexes", + "table": "location_change_event" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + }, + { + "value": "action", + "isExpression": false + }, + { + "value": "resource", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "permission_project_action_resource_idx", + "entityType": "indexes", + "table": "permission" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "message_session_time_created_id_idx", + "entityType": "indexes", + "table": "message" + }, + { + "columns": [ + { + "value": "message_id", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_message_id_id_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "part_session_idx", + "entityType": "indexes", + "table": "part" + }, + { + "columns": [ + { + "value": "source_session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_fork_admission_source_idx", + "entityType": "indexes", + "table": "session_fork_admission" + }, + { + "columns": [ + { + "value": "state", + "isExpression": false + }, + { + "value": "time_updated", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_fork_admission_recovery_idx", + "entityType": "indexes", + "table": "session_fork_admission" + }, + { + "columns": [ + { + "value": "source_session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_fork_intent_source_idx", + "entityType": "indexes", + "table": "session_fork_intent" + }, + { + "columns": [ + { + "value": "state", + "isExpression": false + }, + { + "value": "time_updated", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_fork_intent_delivery_idx", + "entityType": "indexes", + "table": "session_fork_intent" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + }, + { + "value": "delivery", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_input_session_pending_delivery_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "admitted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_admitted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "promoted_seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_input_session_promoted_seq_idx", + "entityType": "indexes", + "table": "session_input" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "intent_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_intent_session_intent_idx", + "entityType": "indexes", + "table": "session_intent" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "state", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_intent_session_state_idx", + "entityType": "indexes", + "table": "session_intent" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_message_session_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "type", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_type_seq_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_session_time_created_id_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_message_time_created_idx", + "entityType": "indexes", + "table": "session_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "prompt_epoch", + "isExpression": false + }, + { + "value": "message_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_prompt_epoch_message_identity_idx", + "entityType": "indexes", + "table": "session_prompt_epoch_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "prompt_epoch", + "isExpression": false + }, + { + "value": "message_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_prompt_epoch_message_lookup_idx", + "entityType": "indexes", + "table": "session_prompt_epoch_message" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "consumed_seq", + "isExpression": false + }, + { + "value": "seq", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_steer_session_pending_seq_idx", + "entityType": "indexes", + "table": "session_steer" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "correlation_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "session_steer_session_correlation_idx", + "entityType": "indexes", + "table": "session_steer" + }, + { + "columns": [ + { + "value": "project_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_project_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "workspace_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_workspace_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "parent_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_parent_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "time_suspended", + "isExpression": false + } + ], + "isUnique": false, + "where": "\"session\".\"time_suspended\" is not null", + "origin": "manual", + "name": "session_time_suspended_idx", + "entityType": "indexes", + "table": "session" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_tool_request_resolution_command_session_idx", + "entityType": "indexes", + "table": "session_tool_request_resolution_command" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "created_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_tool_request_resolution_session_idx", + "entityType": "indexes", + "table": "session_tool_request_resolution" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + }, + { + "value": "prompt_epoch", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "session_world_state_baseline_epoch_idx", + "entityType": "indexes", + "table": "session_world_state_baseline" + }, + { + "columns": [ + { + "value": "run_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "task_admission_run_idx", + "entityType": "indexes", + "table": "task_admission" + }, + { + "columns": [ + { + "value": "status", + "isExpression": false + }, + { + "value": "available_at", + "isExpression": false + }, + { + "value": "lease_expires_at", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "task_notification_outbox_due_idx", + "entityType": "indexes", + "table": "task_notification_outbox" + }, + { + "columns": [ + { + "value": "parent_session_id", + "isExpression": false + } + ], + "isUnique": true, + "where": "\"task_notification_outbox\".\"status\" = 'processing'", + "origin": "manual", + "name": "task_notification_outbox_parent_processing_idx", + "entityType": "indexes", + "table": "task_notification_outbox" + }, + { + "columns": [ + { + "value": "run_id", + "isExpression": false + }, + { + "value": "version", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "task_run_event_run_version_idx", + "entityType": "indexes", + "table": "task_run_event" + }, + { + "columns": [ + { + "value": "time_created", + "isExpression": false + }, + { + "value": "event_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "task_run_event_time_idx", + "entityType": "indexes", + "table": "task_run_event" + }, + { + "columns": [ + { + "value": "child_session_id", + "isExpression": false + }, + { + "value": "generation", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "task_run_child_generation_idx", + "entityType": "indexes", + "table": "task_run" + }, + { + "columns": [ + { + "value": "child_session_id", + "isExpression": false + } + ], + "isUnique": true, + "where": "\"task_run\".\"state\" IN ('admitted', 'provisioning', 'running', 'researching', 'finalizing')", + "origin": "manual", + "name": "task_run_child_active_idx", + "entityType": "indexes", + "table": "task_run" + }, + { + "columns": [ + { + "value": "parent_session_id", + "isExpression": false + }, + { + "value": "state", + "isExpression": false + }, + { + "value": "time_updated", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "task_run_parent_state_idx", + "entityType": "indexes", + "table": "task_run" + }, + { + "columns": [ + { + "value": "root_run_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "task_run_root_idx", + "entityType": "indexes", + "table": "task_run" + }, + { + "columns": [ + { + "value": "state", + "isExpression": false + }, + { + "value": "available_at", + "isExpression": false + }, + { + "value": "priority", + "isExpression": false + }, + { + "value": "time_created", + "isExpression": false + }, + { + "value": "generation", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "task_run_queue_idx", + "entityType": "indexes", + "table": "task_run" + }, + { + "columns": [ + { + "value": "goal_id", + "isExpression": false + }, + { + "value": "goal_tick_seq", + "isExpression": false + }, + { + "value": "goal_role", + "isExpression": false + }, + { + "value": "goal_ordinal", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "task_run_goal_idx", + "entityType": "indexes", + "table": "task_run" + }, + { + "columns": [ + { + "value": "response_message_id", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "task_structured_finalizer_response_message_idx", + "entityType": "indexes", + "table": "task_structured_finalizer_response" + }, + { + "columns": [ + { + "value": "run_id", + "isExpression": false + }, + { + "value": "role", + "isExpression": false + }, + { + "value": "ordinal", + "isExpression": false + } + ], + "isUnique": true, + "where": null, + "origin": "manual", + "name": "task_structured_output_evidence_part_ordinal_idx", + "entityType": "indexes", + "table": "task_structured_output_evidence_part" + }, + { + "columns": [ + { + "value": "part_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "task_structured_output_evidence_part_part_idx", + "entityType": "indexes", + "table": "task_structured_output_evidence_part" + }, + { + "columns": [ + { + "value": "raw_result_message_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "task_structured_output_evidence_raw_message_idx", + "entityType": "indexes", + "table": "task_structured_output_evidence" + }, + { + "columns": [ + { + "value": "result_message_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "task_structured_output_evidence_result_message_idx", + "entityType": "indexes", + "table": "task_structured_output_evidence" + }, + { + "columns": [ + { + "value": "session_id", + "isExpression": false + } + ], + "isUnique": false, + "where": null, + "origin": "manual", + "name": "todo_session_idx", + "entityType": "indexes", + "table": "todo" + }, + { + "columns": [ + "body_hash" + ], + "nameExplicit": false, + "name": "file_part_artifact_body_hash_unique", + "entityType": "uniques", + "table": "file_part_artifact" + }, + { + "columns": [ + "legacy_receipt_id" + ], + "nameExplicit": false, + "name": "session_v2_provider_parity_baseline_legacy_receipt_id_unique", + "entityType": "uniques", + "table": "session_v2_provider_parity_baseline" + }, + { + "columns": [ + "event_id" + ], + "nameExplicit": false, + "name": "event_sync_index_event_id_unique", + "entityType": "uniques", + "table": "event_sync_index" + }, + { + "columns": [ + "target_session_id" + ], + "nameExplicit": false, + "name": "session_fork_admission_target_session_id_unique", + "entityType": "uniques", + "table": "session_fork_admission" + }, + { + "columns": [ + "target_session_id" + ], + "nameExplicit": false, + "name": "session_fork_intent_target_session_id_unique", + "entityType": "uniques", + "table": "session_fork_intent" + }, + { + "columns": [ + "resolution_id" + ], + "nameExplicit": false, + "name": "session_prompt_epoch_recovery_resolution_id_unique", + "entityType": "uniques", + "table": "session_prompt_epoch_recovery" + }, + { + "columns": [ + "id" + ], + "nameExplicit": false, + "name": "session_steer_id_unique", + "entityType": "uniques", + "table": "session_steer" + }, + { + "columns": [ + "receipt_id" + ], + "nameExplicit": false, + "name": "session_tool_request_resolution_receipt_id_unique", + "entityType": "uniques", + "table": "session_tool_request_resolution" + }, + { + "columns": [ + "run_id" + ], + "nameExplicit": false, + "name": "task_notification_outbox_run_id_unique", + "entityType": "uniques", + "table": "task_notification_outbox" + }, + { + "columns": [ + "message_id" + ], + "nameExplicit": false, + "name": "task_notification_outbox_message_id_unique", + "entityType": "uniques", + "table": "task_notification_outbox" + }, + { + "value": "\"trigger\" IN ('idle', 'pause', 'project_switch', 'session_finalization')", + "name": "learning_admission_outbox_trigger_check", + "entityType": "checks", + "table": "learning_admission_outbox" + }, + { + "value": "json_valid(\"payload_json\") AND json_type(\"payload_json\") = 'object'", + "name": "learning_admission_outbox_payload_json_check", + "entityType": "checks", + "table": "learning_admission_outbox" + }, + { + "value": "length(\"payload_fingerprint\") = 64 AND \"payload_fingerprint\" NOT GLOB '*[^0-9a-f]*'", + "name": "learning_admission_outbox_payload_fingerprint_check", + "entityType": "checks", + "table": "learning_admission_outbox" + }, + { + "value": "\"state\" IN ('pending', 'admitted', 'rejected')", + "name": "learning_admission_outbox_state_check", + "entityType": "checks", + "table": "learning_admission_outbox" + }, + { + "value": "(\"state\" = 'pending' AND \"job_id\" IS NULL AND \"candidate_input_ref\" IS NULL AND \"rejection_code\" IS NULL AND \"rejection_detail\" IS NULL AND \"settled_at\" IS NULL) OR (\"state\" = 'admitted' AND \"job_id\" IS NOT NULL AND length(trim(\"candidate_input_ref\")) > 0 AND \"rejection_code\" IS NULL AND \"rejection_detail\" IS NULL AND \"settled_at\" IS NOT NULL) OR (\"state\" = 'rejected' AND \"job_id\" IS NULL AND length(trim(\"rejection_code\")) > 0 AND length(trim(\"rejection_detail\")) > 0 AND \"settled_at\" IS NOT NULL)", + "name": "learning_admission_outbox_settlement_check", + "entityType": "checks", + "table": "learning_admission_outbox" + }, + { + "value": "\"sequence\" >= 0", + "name": "learning_governance_action_sequence_check", + "entityType": "checks", + "table": "learning_governance_action" + }, + { + "value": "\"version\" >= 0", + "name": "learning_governance_action_version_check", + "entityType": "checks", + "table": "learning_governance_action" + }, + { + "value": "length(trim(\"candidate_id\")) > 0", + "name": "learning_governance_action_candidate_check", + "entityType": "checks", + "table": "learning_governance_action" + }, + { + "value": "json_valid(\"payload_json\")", + "name": "learning_governance_action_payload_json_check", + "entityType": "checks", + "table": "learning_governance_action" + }, + { + "value": "\"kind\" IN ('document_stage', 'memory_inbox')", + "name": "learning_governance_action_kind_check", + "entityType": "checks", + "table": "learning_governance_action" + }, + { + "value": "length(\"payload_fingerprint\") = 64 AND \"payload_fingerprint\" NOT GLOB '*[^0-9a-f]*'", + "name": "learning_governance_action_payload_fingerprint_check", + "entityType": "checks", + "table": "learning_governance_action" + }, + { + "value": "\"result_hash\" IS NULL OR (length(\"result_hash\") = 64 AND \"result_hash\" NOT GLOB '*[^0-9a-f]*')", + "name": "learning_governance_action_result_hash_check", + "entityType": "checks", + "table": "learning_governance_action" + }, + { + "value": "\"result_fingerprint\" IS NULL OR (length(\"result_fingerprint\") = 64 AND \"result_fingerprint\" NOT GLOB '*[^0-9a-f]*')", + "name": "learning_governance_action_result_fingerprint_check", + "entityType": "checks", + "table": "learning_governance_action" + }, + { + "value": "\"state\" IN ('prepared', 'running', 'settled', 'recovery_required')", + "name": "learning_governance_action_state_check", + "entityType": "checks", + "table": "learning_governance_action" + }, + { + "value": "(\"state\" = 'prepared' AND \"owner\" IS NULL AND \"lease_expires_at\" IS NULL AND \"result_ref\" IS NULL AND \"result_hash\" IS NULL AND \"result_fingerprint\" IS NULL AND \"error_code\" IS NULL AND \"settled_at\" IS NULL) OR (\"state\" = 'running' AND length(trim(\"owner\")) > 0 AND \"lease_expires_at\" IS NOT NULL AND \"result_ref\" IS NULL AND \"result_hash\" IS NULL AND \"result_fingerprint\" IS NULL AND \"error_code\" IS NULL AND \"settled_at\" IS NULL) OR (\"state\" = 'settled' AND \"owner\" IS NULL AND \"lease_expires_at\" IS NULL AND length(trim(\"result_ref\")) > 0 AND \"result_hash\" IS NOT NULL AND \"result_fingerprint\" IS NOT NULL AND \"error_code\" IS NULL AND \"settled_at\" IS NOT NULL) OR (\"state\" = 'recovery_required' AND \"owner\" IS NULL AND \"lease_expires_at\" IS NULL AND length(trim(\"error_code\")) > 0 AND \"settled_at\" IS NOT NULL)", + "name": "learning_governance_action_lifecycle_check", + "entityType": "checks", + "table": "learning_governance_action" + }, + { + "value": "\"sequence\" >= 0", + "name": "learning_governance_compensation_sequence_check", + "entityType": "checks", + "table": "learning_governance_compensation" + }, + { + "value": "\"version\" >= 0", + "name": "learning_governance_compensation_version_check", + "entityType": "checks", + "table": "learning_governance_compensation" + }, + { + "value": "\"kind\" IN ('document_quarantine', 'memory_inbox_revoke')", + "name": "learning_governance_compensation_kind_check", + "entityType": "checks", + "table": "learning_governance_compensation" + }, + { + "value": "length(\"source_payload_fingerprint\") = 64 AND \"source_payload_fingerprint\" NOT GLOB '*[^0-9a-f]*'", + "name": "learning_governance_compensation_source_fingerprint_check", + "entityType": "checks", + "table": "learning_governance_compensation" + }, + { + "value": "\"result_hash\" IS NULL OR (length(\"result_hash\") = 64 AND \"result_hash\" NOT GLOB '*[^0-9a-f]*')", + "name": "learning_governance_compensation_result_hash_check", + "entityType": "checks", + "table": "learning_governance_compensation" + }, + { + "value": "\"result_fingerprint\" IS NULL OR (length(\"result_fingerprint\") = 64 AND \"result_fingerprint\" NOT GLOB '*[^0-9a-f]*')", + "name": "learning_governance_compensation_result_fingerprint_check", + "entityType": "checks", + "table": "learning_governance_compensation" + }, + { + "value": "\"state\" IN ('prepared', 'running', 'settled', 'recovery_required')", + "name": "learning_governance_compensation_state_check", + "entityType": "checks", + "table": "learning_governance_compensation" + }, + { + "value": "(\"state\" = 'prepared' AND \"owner\" IS NULL AND \"lease_expires_at\" IS NULL AND \"result_ref\" IS NULL AND \"result_hash\" IS NULL AND \"result_fingerprint\" IS NULL AND \"error_code\" IS NULL AND \"settled_at\" IS NULL) OR (\"state\" = 'running' AND length(trim(\"owner\")) > 0 AND \"lease_expires_at\" IS NOT NULL AND \"result_ref\" IS NULL AND \"result_hash\" IS NULL AND \"result_fingerprint\" IS NULL AND \"error_code\" IS NULL AND \"settled_at\" IS NULL) OR (\"state\" = 'settled' AND \"owner\" IS NULL AND \"lease_expires_at\" IS NULL AND length(trim(\"result_ref\")) > 0 AND \"result_hash\" IS NOT NULL AND \"result_fingerprint\" IS NOT NULL AND \"error_code\" IS NULL AND \"settled_at\" IS NOT NULL) OR (\"state\" = 'recovery_required' AND \"owner\" IS NULL AND \"lease_expires_at\" IS NULL AND length(trim(\"error_code\")) > 0 AND \"settled_at\" IS NOT NULL)", + "name": "learning_governance_compensation_lifecycle_check", + "entityType": "checks", + "table": "learning_governance_compensation" + }, + { + "value": "\"policy\" = 'manual_review'", + "name": "learning_governance_plan_policy_check", + "entityType": "checks", + "table": "learning_governance_plan" + }, + { + "value": "\"action_count\" >= 0", + "name": "learning_governance_plan_action_count_check", + "entityType": "checks", + "table": "learning_governance_plan" + }, + { + "value": "\"version\" >= 0", + "name": "learning_governance_plan_version_check", + "entityType": "checks", + "table": "learning_governance_plan" + }, + { + "value": "json_valid(\"payload_json\")", + "name": "learning_governance_plan_payload_json_check", + "entityType": "checks", + "table": "learning_governance_plan" + }, + { + "value": "length(\"payload_fingerprint\") = 64 AND \"payload_fingerprint\" NOT GLOB '*[^0-9a-f]*'", + "name": "learning_governance_plan_payload_fingerprint_check", + "entityType": "checks", + "table": "learning_governance_plan" + }, + { + "value": "\"result_hash\" IS NULL OR (length(\"result_hash\") = 64 AND \"result_hash\" NOT GLOB '*[^0-9a-f]*')", + "name": "learning_governance_plan_result_hash_check", + "entityType": "checks", + "table": "learning_governance_plan" + }, + { + "value": "\"result_fingerprint\" IS NULL OR (length(\"result_fingerprint\") = 64 AND \"result_fingerprint\" NOT GLOB '*[^0-9a-f]*')", + "name": "learning_governance_plan_result_fingerprint_check", + "entityType": "checks", + "table": "learning_governance_plan" + }, + { + "value": "\"state\" IN ('prepared', 'settled', 'recovery_required')", + "name": "learning_governance_plan_state_check", + "entityType": "checks", + "table": "learning_governance_plan" + }, + { + "value": "(\"state\" = 'prepared' AND \"result_ref\" IS NULL AND \"result_hash\" IS NULL AND \"result_fingerprint\" IS NULL AND \"error_code\" IS NULL AND \"settled_at\" IS NULL) OR (\"state\" = 'settled' AND length(trim(\"result_ref\")) > 0 AND \"result_hash\" IS NOT NULL AND \"result_fingerprint\" IS NOT NULL AND \"error_code\" IS NULL AND \"settled_at\" IS NOT NULL) OR (\"state\" = 'recovery_required' AND length(trim(\"error_code\")) > 0 AND \"settled_at\" IS NOT NULL)", + "name": "learning_governance_plan_settlement_check", + "entityType": "checks", + "table": "learning_governance_plan" + }, + { + "value": "\"trigger\" IN ('idle', 'pause', 'project_switch', 'session_finalization')", + "name": "learning_job_trigger_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"policy\" IN ('auto_merge_safe_project', 'manual_review')", + "name": "learning_job_policy_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"state\" IN ('queued', 'running', 'reviewing', 'governance', 'completed', 'failed', 'cancelled', 'recovery_required')", + "name": "learning_job_state_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"attempts\" >= 0", + "name": "learning_job_attempts_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"max_attempts\" > 0", + "name": "learning_job_max_attempts_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"version\" >= 0", + "name": "learning_job_version_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "length(\"admission_fingerprint\") = 64 AND \"admission_fingerprint\" NOT GLOB '*[^0-9a-f]*'", + "name": "learning_job_admission_fingerprint_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "(\"side_effect_state\" = 'not_started' AND \"side_effect_kind\" IS NULL) OR (\"side_effect_state\" IN ('started', 'settled', 'unknown') AND \"side_effect_kind\" IS NOT NULL)", + "name": "learning_job_side_effect_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "(\"side_effect_state\" = 'not_started' AND \"expected_result_ref\" IS NULL) OR (\"side_effect_kind\" IN ('extraction', 'reviewer') AND \"side_effect_state\" IN ('started', 'settled') AND length(trim(\"expected_result_ref\")) > 0) OR (\"side_effect_kind\" = 'governance' AND \"expected_result_ref\" IS NULL) OR (\"side_effect_state\" = 'unknown' AND \"expected_result_ref\" IS NULL)", + "name": "learning_job_expected_result_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"side_effect_state\" <> 'settled' OR (length(trim(\"result_ref\")) > 0 AND (\"expected_result_ref\" IS NULL OR \"side_effect_kind\" = 'reviewer' OR \"result_ref\" = \"expected_result_ref\"))", + "name": "learning_job_settled_result_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"state\" NOT IN ('running', 'reviewing', 'governance') OR \"side_effect_state\" = 'not_started' OR (\"state\" = 'running' AND \"side_effect_kind\" = 'extraction') OR (\"state\" = 'reviewing' AND \"side_effect_kind\" = 'reviewer') OR (\"state\" = 'governance' AND \"side_effect_kind\" = 'governance')", + "name": "learning_job_active_phase_kind_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "(\"state\" = 'queued' AND \"owner\" IS NULL AND \"lease_expires_at\" IS NULL AND \"side_effect_state\" = 'not_started') OR (\"state\" IN ('running', 'reviewing', 'governance') AND \"started_at\" IS NOT NULL AND \"settled_at\" IS NULL AND ((length(trim(\"owner\")) > 0 AND \"lease_expires_at\" IS NOT NULL) OR (\"state\" IN ('reviewing', 'governance') AND \"owner\" IS NULL AND \"lease_expires_at\" IS NULL AND \"side_effect_state\" = 'not_started'))) OR (\"state\" IN ('completed', 'failed', 'cancelled', 'recovery_required') AND \"owner\" IS NULL AND \"lease_expires_at\" IS NULL AND \"settled_at\" IS NOT NULL)", + "name": "learning_job_ownership_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"state\" <> 'recovery_required' OR (\"side_effect_state\" <> 'not_started' AND \"error_code\" IS NOT NULL)", + "name": "learning_job_recovery_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"state\" <> 'completed' OR (\"side_effect_state\" = 'settled' AND length(trim(\"result_ref\")) > 0)", + "name": "learning_job_completed_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"state\" <> 'failed' OR length(trim(\"error_code\")) > 0", + "name": "learning_job_failed_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"state\" NOT IN ('completed', 'failed', 'cancelled') OR \"side_effect_state\" IN ('not_started', 'settled')", + "name": "learning_job_terminal_side_effect_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"settlement_fingerprint\" IS NULL OR (length(\"settlement_fingerprint\") = 64 AND \"settlement_fingerprint\" NOT GLOB '*[^0-9a-f]*')", + "name": "learning_job_settlement_fingerprint_check", + "entityType": "checks", + "table": "learning_job" + }, + { + "value": "\"trigger\" IN ('idle', 'pause', 'project_switch')", + "name": "learning_lifecycle_trigger_kind_check", + "entityType": "checks", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "value": "\"state\" IN ('prepared', 'admitted')", + "name": "learning_lifecycle_trigger_state_check", + "entityType": "checks", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "value": "length(\"source_admission_hash\") = 64 AND \"source_admission_hash\" NOT GLOB '*[^0-9a-f]*'", + "name": "learning_lifecycle_trigger_source_admission_hash_check", + "entityType": "checks", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "value": "length(\"source_terminal_hash\") = 64 AND \"source_terminal_hash\" NOT GLOB '*[^0-9a-f]*'", + "name": "learning_lifecycle_trigger_source_terminal_hash_check", + "entityType": "checks", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "value": "length(\"artifact_hash\") = 64 AND \"artifact_hash\" NOT GLOB '*[^0-9a-f]*'", + "name": "learning_lifecycle_trigger_artifact_hash_check", + "entityType": "checks", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "value": "length(\"admission_fingerprint\") = 64 AND \"admission_fingerprint\" NOT GLOB '*[^0-9a-f]*'", + "name": "learning_lifecycle_trigger_admission_fingerprint_check", + "entityType": "checks", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "value": "json_valid(\"admission_json\") AND json_type(\"admission_json\") = 'object'", + "name": "learning_lifecycle_trigger_admission_json_check", + "entityType": "checks", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "value": "json_valid(\"artifact_json\") AND json_type(\"artifact_json\") = 'object'", + "name": "learning_lifecycle_trigger_artifact_json_check", + "entityType": "checks", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "value": "(\"state\" = 'prepared' AND \"settled_at\" IS NULL) OR (\"state\" = 'admitted' AND \"settled_at\" IS NOT NULL AND \"error_detail\" IS NULL)", + "name": "learning_lifecycle_trigger_settlement_check", + "entityType": "checks", + "table": "learning_lifecycle_trigger_receipt" + }, + { + "value": "\"state\" IN ('prepared', 'dispatching', 'settled', 'failed', 'recovery_required')", + "name": "learning_reviewer_attempt_state_check", + "entityType": "checks", + "table": "learning_reviewer_attempt" + }, + { + "value": "\"version\" >= 0", + "name": "learning_reviewer_attempt_version_check", + "entityType": "checks", + "table": "learning_reviewer_attempt" + }, + { + "value": "length(\"request_hash\") = 64 AND \"request_hash\" NOT GLOB '*[^0-9a-f]*' AND length(\"source_candidate_set_hash\") = 64 AND \"source_candidate_set_hash\" NOT GLOB '*[^0-9a-f]*' AND length(\"policy_hash\") = 64 AND \"policy_hash\" NOT GLOB '*[^0-9a-f]*'", + "name": "learning_reviewer_attempt_hash_check", + "entityType": "checks", + "table": "learning_reviewer_attempt" + }, + { + "value": "(\"state\" IN ('prepared', 'dispatching') AND \"settled_at\" IS NULL) OR (\"state\" IN ('settled', 'failed', 'recovery_required') AND \"settled_at\" IS NOT NULL)", + "name": "learning_reviewer_attempt_terminal_check", + "entityType": "checks", + "table": "learning_reviewer_attempt" + }, + { + "value": "(\"state\" <> 'settled' AND \"response_ref\" IS NULL AND \"response_hash\" IS NULL AND \"verdict\" IS NULL AND \"selected_candidate_ids_json\" IS NULL AND \"selected_subset_hash\" IS NULL) OR (\"state\" = 'settled' AND length(trim(\"response_ref\")) > 0 AND length(\"response_hash\") = 64 AND \"response_hash\" NOT GLOB '*[^0-9a-f]*' AND \"verdict\" IN ('approve', 'reject', 'manual_review') AND length(trim(\"selected_candidate_ids_json\")) > 0 AND length(\"selected_subset_hash\") = 64 AND \"selected_subset_hash\" NOT GLOB '*[^0-9a-f]*')", + "name": "learning_reviewer_attempt_response_check", + "entityType": "checks", + "table": "learning_reviewer_attempt" + }, + { + "value": "\"state\" NOT IN ('failed', 'recovery_required') OR length(trim(\"error_code\")) > 0", + "name": "learning_reviewer_attempt_error_check", + "entityType": "checks", + "table": "learning_reviewer_attempt" + }, + { + "value": "length(\"matrix_hash\") = 64 AND \"matrix_hash\" NOT GLOB '*[^0-9a-f]*'", + "name": "released_knowledge_evaluation_matrix_hash_check", + "entityType": "checks", + "table": "released_knowledge_evaluation" + }, + { + "value": "json_valid(\"matrix_json\")", + "name": "released_knowledge_evaluation_matrix_json_check", + "entityType": "checks", + "table": "released_knowledge_evaluation" + }, + { + "value": "json_valid(\"document_manifest_json\") AND json_type(\"document_manifest_json\") = 'array'", + "name": "released_knowledge_evaluation_document_manifest_json_check", + "entityType": "checks", + "table": "released_knowledge_evaluation" + }, + { + "value": "\"repetitions\" > 0", + "name": "released_knowledge_evaluation_repetitions_check", + "entityType": "checks", + "table": "released_knowledge_evaluation" + }, + { + "value": "\"evaluator_type\" IN ('human', 'agent', 'system')", + "name": "released_knowledge_evaluation_actor_type_check", + "entityType": "checks", + "table": "released_knowledge_evaluation" + }, + { + "value": "\"ordinal\" >= 0", + "name": "released_knowledge_snapshot_document_ordinal_check", + "entityType": "checks", + "table": "released_knowledge_snapshot_document" + }, + { + "value": "\"source_store\" IN ('user_global', 'project')", + "name": "released_knowledge_snapshot_document_source_store_check", + "entityType": "checks", + "table": "released_knowledge_snapshot_document" + }, + { + "value": "\"doc_version\" > 0", + "name": "released_knowledge_snapshot_document_version_check", + "entityType": "checks", + "table": "released_knowledge_snapshot_document" + }, + { + "value": "length(\"doc_hash\") = 71 AND substr(\"doc_hash\", 1, 7) = 'sha256:' AND substr(\"doc_hash\", 8) NOT GLOB '*[^0-9a-f]*'", + "name": "released_knowledge_snapshot_document_hash_check", + "entityType": "checks", + "table": "released_knowledge_snapshot_document" + }, + { + "value": "\"doc_type\" IN ('knowledge', 'strategy', 'methodology', 'memory', 'skill')", + "name": "released_knowledge_snapshot_document_type_check", + "entityType": "checks", + "table": "released_knowledge_snapshot_document" + }, + { + "value": "(\"snapshot_id\" IS NULL AND \"generation\" = 0) OR (\"snapshot_id\" IS NOT NULL AND \"generation\" > 0)", + "name": "released_knowledge_snapshot_head_generation_check", + "entityType": "checks", + "table": "released_knowledge_snapshot_head" + }, + { + "value": "\"release_kind\" IN ('legacy_baseline', 'evaluated', 'rollback')", + "name": "released_knowledge_snapshot_release_kind_check", + "entityType": "checks", + "table": "released_knowledge_snapshot" + }, + { + "value": "\"document_count\" >= 0", + "name": "released_knowledge_snapshot_document_count_check", + "entityType": "checks", + "table": "released_knowledge_snapshot" + }, + { + "value": "(\"verdict\" = 'passed' AND \"published_generation\" > 0) OR (\"verdict\" = 'failed' AND \"published_generation\" >= 0)", + "name": "released_knowledge_snapshot_published_generation_check", + "entityType": "checks", + "table": "released_knowledge_snapshot" + }, + { + "value": "\"verdict\" IN ('passed', 'failed')", + "name": "released_knowledge_snapshot_verdict_check", + "entityType": "checks", + "table": "released_knowledge_snapshot" + }, + { + "value": "(\"verdict\" = 'passed' AND \"failure_reason\" IS NULL) OR (\"verdict\" = 'failed' AND length(trim(\"failure_reason\")) > 0)", + "name": "released_knowledge_snapshot_failure_reason_check", + "entityType": "checks", + "table": "released_knowledge_snapshot" + }, + { + "value": "(\"release_kind\" = 'legacy_baseline' AND \"parent_snapshot_id\" IS NULL AND \"verdict\" = 'passed') OR (\"release_kind\" <> 'legacy_baseline' AND \"parent_snapshot_id\" IS NOT NULL)", + "name": "released_knowledge_snapshot_release_chain_check", + "entityType": "checks", + "table": "released_knowledge_snapshot" + }, + { + "value": "\"release_kind\" <> 'evaluated' OR \"verdict\" = 'failed' OR \"document_count\" > 0", + "name": "released_knowledge_snapshot_evaluated_membership_check", + "entityType": "checks", + "table": "released_knowledge_snapshot" + }, + { + "value": "\"actor_type\" IN ('human', 'agent', 'system')", + "name": "released_knowledge_snapshot_actor_type_check", + "entityType": "checks", + "table": "released_knowledge_snapshot" + } + ], + "renames": [] +} \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index cffe3aa2..ea328b62 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/package.json", - "version": "4.0.7-r2", + "version": "4.0.8", "name": "@deepagent-code/core", "type": "module", "license": "AGPL-3.0-or-later", diff --git a/packages/core/script/release-gate-migration-check.ts b/packages/core/script/release-gate-migration-check.ts new file mode 100644 index 00000000..7fe123c9 --- /dev/null +++ b/packages/core/script/release-gate-migration-check.ts @@ -0,0 +1,21 @@ +// Release-gate migration check: open a DB copy via layerFromPath (which applies pending +// migrations) and report the latest applied migrations. Never touches the live DB. +import { Effect } from "effect" +import { Database } from "../src/database/database" + +const filename = process.argv[2] +if (!filename) { + console.error("usage: bun script/release-gate-migration-check.ts ") + process.exit(1) +} + +const program = Effect.gen(function* () { + const svc = yield* Database.Service + const rows = yield* svc.db.all<{ id: string }>("SELECT id FROM migration ORDER BY id DESC LIMIT 6") + console.log("APPLIED_OK") + for (const row of rows) console.log(" migration:", row.id) + const count = yield* svc.db.get<{ n: number }>("SELECT COUNT(*) AS n FROM migration") + console.log("total migrations:", count?.n) +}).pipe(Effect.provide(Database.layerFromPath(filename))) + +await Effect.runPromise(program) diff --git a/packages/core/src/agent-gateway.ts b/packages/core/src/agent-gateway.ts index 6944769d..49e04155 100644 --- a/packages/core/src/agent-gateway.ts +++ b/packages/core/src/agent-gateway.ts @@ -17,6 +17,7 @@ import { knowledgeEnabled, strategyMethodologyEnabled, domainKnowledgeEnabled, m import type { AgentMode } from "./deepagent/mode" import { resolveDeepAgentCodeHome } from "./deepagent/workspace" import * as KnowledgeRetriever from "./deepagent/knowledge-retriever" +import { buildProfile as buildRunProfile } from "./deepagent/profile-builder" import type { ProblemProfile } from "./deepagent/domain-pack" import { DeepAgentDurableLearning, type Admission } from "./deepagent/durable-learning" import { writeFileAtomic } from "./deepagent/atomic-write" @@ -543,6 +544,7 @@ import * as DeepAgentKnowledgeSource from "./deepagent/knowledge-source" import * as DeepAgentKnowledgeSeed from "./deepagent/knowledge-seed" import * as DeepAgentDurableKnowledgeStore from "./deepagent/durable-knowledge-store" import * as DeepAgentDomainPackRegistry from "./deepagent/domain-pack-registry" +import { readPinnedPacks } from "./deepagent/pinned-packs" import * as DeepAgentPromotion from "./deepagent/promotion" import * as DeepAgentDocumentStore from "./deepagent/document-store" import * as DeepAgentRunGraph from "./deepagent/run-graph" @@ -917,6 +919,12 @@ const ensureSessionStateForRun = (run: RunRecord): DeepAgentSessionState.Session runId: run.runID, workspacePath: workspacePathForInput(run.input) ?? existing?.workspacePath ?? process.cwd(), userRequest: userRequestForInput(run.input) ?? existing?.userRequest ?? null, + // FEAT-007: lock the run's active pack snapshot id onto the session state at the run entry, so + // the session prompt loop can attribute each tool-request receipt's + // context_active_pack_set_snapshot_id to the exact pack set that shaped this run's knowledge + // retrieval. Single authority (FEAT-002): activePackSnapshot is memoized per run and derives from + // the SAME activation the retriever constrains on, so the receipt never re-derives it. + packSnapshotId: activePackSnapshot(run).id, }) } @@ -1769,6 +1777,9 @@ const modelWorkPackage = (run: RunRecord) => { const selectedStrategyRefs = refIdsOfKind("strategy") const selectedMemoryRefs = refIdsOfKind("memory") const selectedMethodologyRefs = refIdsOfKind("methodology") + // FEAT-002: compute the pack snapshot ONCE per work package (previously 2+ calls per record + // build); the WeakMap inside activePackSnapshot keeps every other call site on the same value. + const packSnapshot = activePackSnapshot(run) return { schema_version: "model_work_package.v1", work_package_id: run.workPackageID, @@ -1953,9 +1964,10 @@ const modelWorkPackage = (run: RunRecord) => { required_skill_refs: [], selected_methodology_refs: selectedMethodologyRefs, // docs/34 §9 S4/DAP-7: record the active domain pack set + locked snapshot so the run is - // reproducible. activePackSnapshot is derived from the run's problem profile via the registry. - active_pack_set: activePackSnapshot(run).packs.map((p) => p.id), - pack_snapshot_id: activePackSnapshot(run).id, + // reproducible. FEAT-002: derived from the run's single authoritative profile (same one + // retrieval consumed), so active_pack_set and the retrieval constraints share one source. + active_pack_set: packSnapshot.packs.map((p) => p.id), + pack_snapshot_id: packSnapshot.id, knowledge_retrieval: { enabled: knowledgeEnabled(run.agentMode), mode: knowledgeEnabled(run.agentMode) ? "bounded_retrieval_refs_only" : "disabled", @@ -2986,6 +2998,10 @@ function parseAgentMode(value: string | undefined): AgentMode { const retrieveKnowledge = (run: RunRecord) => { const task = extractTaskContext(run) const workspacePath = workspacePathForInput(run.input) + // FEAT-001: GUI-pinned packs must reach retrieval. Pins persist in the workspace memory dir + // (/memory/pinned-packs.json) written by the packsPin/packsUnpin handlers; the gateway + // reads the SAME dir so the control plane and the runtime agree. + const pinned = pinnedPackIds() return KnowledgeRetriever.retrieve({ mode: run.agentMode, task, @@ -2993,6 +3009,11 @@ const retrieveKnowledge = (run: RunRecord) => { round: 1, previousFailures: 0, profile: extractProblemProfile(run), + // FEAT-002: the authoritative run profile (built once per run). The retriever uses it for + // pack activation instead of re-deriving one from task text, so retrieval's selected packs + // and the run record's active_pack_set come from the same profile. + profileOverride: profileForRun(run), + ...(pinned.length > 0 ? { domainOptions: { overrides: pinned } } : {}), // docs/34 §8: scope durable retrieval to the run's workspace path (unions user-global + // this workspace's project-shared). Absent workspace => user-global only. ...(workspacePath ? { workspacePath } : {}), @@ -3058,35 +3079,60 @@ const extractProblemProfile = (run: RunRecord): ProblemProfile => { } } +// FEAT-001: the workspace memory dir the HTTP handlers (deepagent-code) write pinned-packs.json to +// is /memory (dirname(runsDir)/memory == Global.Path.agent.data/memory in +// production). The gateway derives it from its own configured storage home — no workspacePath +// needed, since the pin file is per-instance, mirroring the handler's workspaceMemoryDir(). +const pinnedMemoryDir = (): string => path.join(path.resolve(current.baseDir ?? resolveDeepAgentCodeHome()), "memory") + +const pinnedPackIds = (): string[] => readPinnedPacks(pinnedMemoryDir()) + +// FEAT-002: the SINGLE authoritative ExtendedProblemProfile producer for a run. The gateway used +// to keep two competing truths — profileFromInput (regex-derived inside the retriever) and a +// hardcoded profile for the recorded snapshot — so active_pack_set could diverge from the packs +// retrieval actually constrained on. Now the gateway builds one profile at run entry (workspace +// facts via profile-builder, the same logic deepagent-code's profile-detector re-exports) and both +// knowledge retrieval (RetrievalInput.profileOverride) and the pack snapshot consume it. Built +// once per run record: inputs are immutable and pins are read once, so the WeakMap cache makes +// every downstream call site observe the identical profile. +const runProfileCache = new WeakMap() +const profileForRun = (run: RunRecord): DeepAgentDomainPackRegistry.ExtendedProblemProfile => { + const cached = runProfileCache.get(run) + if (cached) return cached + const profile = buildRunProfile({ + cwd: workspacePathForInput(run.input) ?? process.cwd(), + agentMode: run.agentMode, + scenarioMode: "intelligence", + userRequest: userRequestForInput(run.input) ?? run.input.feature ?? "", + userOverrides: pinnedPackIds(), + }) + runProfileCache.set(run, profile) + return profile +} + // docs/34 §9 DAP-7: lock the active domain pack snapshot for a run so each run records the exact -// pack set that shaped its knowledge retrieval. Best-effort: if the registry has no configured -// dir or discover returns nothing, the snapshot is empty (no packs) — retrieval still works. +// pack set that shaped its knowledge retrieval. FEAT-002: no hardcoded profile fields — the +// snapshot is locked from the SAME activation (activationForProfile over profileForRun) that the +// retriever constrains on, and memoized per run so the record build calls it effectively once. +// Best-effort: if the registry has no configured dir or discover returns nothing, the snapshot is +// empty (no packs) — retrieval still works. +const packSnapshotCache = new WeakMap() const activePackSnapshot = (run: RunRecord): DeepAgentDomainPackRegistry.PackSnapshot => { + const cached = packSnapshotCache.get(run) + if (cached) return cached + let snapshot: DeepAgentDomainPackRegistry.PackSnapshot if (!DeepAgentDomainPackRegistry.isRegistryConfigured()) { - return { id: "pack_snapshot:empty", packs: [], created_at: new Date().toISOString() } - } - try { - const signals = run.input.feature ?? "" - const profile: DeepAgentDomainPackRegistry.ExtendedProblemProfile = { - scenario_mode: "intelligence", - agent_strength: run.agentMode as DeepAgentDomainPackRegistry.ExtendedProblemProfile["agent_strength"], - task_kind: "implement", - code_domains: ["code"], - business_domains: [], - platforms: [], - languages: ["typescript"], - frameworks: [], - data_classes: [], - risk_markers: [], - repo_signals: [signals], - round_signals: [], - user_overrides: [], + snapshot = { id: "pack_snapshot:empty", packs: [], created_at: new Date().toISOString() } + } else { + try { + const activation = KnowledgeRetriever.activationForProfile(profileForRun(run)) + snapshot = DeepAgentDomainPackRegistry.lockSnapshot(activation.activePackIds) + } catch { + snapshot = { id: "pack_snapshot:error", packs: [], created_at: new Date().toISOString() } } - const { snapshot } = DeepAgentDomainPackRegistry.activateForProfile(profile) - return snapshot - } catch { - return { id: "pack_snapshot:error", packs: [], created_at: new Date().toISOString() } } + packSnapshotCache.set(run, snapshot) + return snapshot } const activationMode = (mode: AgentMode) => diff --git a/packages/core/src/context-federation/provider-owner.ts b/packages/core/src/context-federation/provider-owner.ts index 5082485e..2a5a7046 100644 --- a/packages/core/src/context-federation/provider-owner.ts +++ b/packages/core/src/context-federation/provider-owner.ts @@ -21,6 +21,10 @@ export interface Interface { readonly register: (input: { readonly ownerToken: string readonly leaseMs: number + // Successor generation registration (BUG-407-009): the old token was fenced by lease expiry and + // this token continues the same ownership chain. Also installs the protocol-3 compatibility fence + // so a pre-successor binary refuses to open this database instead of re-quarantining the chain. + readonly successor?: boolean readonly now?: number }) => Effect.Effect readonly heartbeat: (input: { @@ -52,6 +56,7 @@ export const layer = Layer.effect( const register = Effect.fn("SessionProviderOwner.register")(function* (input: { readonly ownerToken: string readonly leaseMs: number + readonly successor?: boolean readonly now?: number }) { return yield* db @@ -92,7 +97,7 @@ export function observedAtInTransaction(tx: Transaction): Effect.Effect function registerInTransaction( tx: Transaction, - input: { readonly ownerToken: string; readonly leaseMs: number }, + input: { readonly ownerToken: string; readonly leaseMs: number; readonly successor?: boolean }, ) { return Effect.gen(function* () { if (!input.ownerToken.trim()) return yield* new ConflictError({ reason: "owner_token_required" }) @@ -110,6 +115,14 @@ function registerInTransaction( .returning() .get() if (!inserted) return yield* new ConflictError({ reason: "owner_token_already_registered" }) + if (input.successor) + yield* tx.run(sql` + INSERT INTO database_capability( + capability, minimum_reader_protocol, minimum_writer_protocol, installed_at + ) VALUES ( + 'provider_owner_successor_v1', 3, 3, ${databaseNow} + ) ON CONFLICT(capability) DO NOTHING + `) return lease(inserted) }) } diff --git a/packages/core/src/context-federation/reconciliation-sql.ts b/packages/core/src/context-federation/reconciliation-sql.ts new file mode 100644 index 00000000..0c7b941a --- /dev/null +++ b/packages/core/src/context-federation/reconciliation-sql.ts @@ -0,0 +1,33 @@ +// FEAT-007: durable per-turn reconciliation records for federated context selections. +// One row per federated provider turn receipt — compares the legacy selected refs (when +// available at the receipt write site) against the projection selection ref set so the +// "why did the model see these documents" question survives restarts (knowledgeMemoryDelta +// is in-memory only). +import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core" +import { SessionTable } from "../session/sql" +import { SessionActivityTable } from "./session-sql" +import type { ContextReconciliation } from "./reconciliation" + +export const SessionContextReconciliationTable = sqliteTable( + "session_context_reconciliation", + { + reconciliation_id: text().primaryKey().notNull(), + session_id: text() + .notNull() + .references(() => SessionTable.id, { onDelete: "cascade" }), + activity_id: text() + .notNull() + .references(() => SessionActivityTable.activity_id, { onDelete: "cascade" }), + turn_receipt_id: text().notNull(), + selection_id: text(), + legacy_refs_fingerprint: text(), + projection_refs_fingerprint: text().notNull(), + outcome: text().$type().notNull(), + diff_summary: text({ mode: "json" }).$type().notNull(), + created_at: integer().notNull(), + }, + (table) => [ + uniqueIndex("session_context_reconciliation_receipt_idx").on(table.turn_receipt_id), + index("session_context_reconciliation_session_idx").on(table.session_id, table.created_at), + ], +) diff --git a/packages/core/src/context-federation/reconciliation.ts b/packages/core/src/context-federation/reconciliation.ts new file mode 100644 index 00000000..60bb84f5 --- /dev/null +++ b/packages/core/src/context-federation/reconciliation.ts @@ -0,0 +1,158 @@ +export * as ContextReconciliation from "./reconciliation" + +import { Effect } from "effect" +import { Database } from "../database/database" +import { Hash } from "../util/hash" +import type { GraphKind } from "./contract" +import { SessionContextReconciliationTable } from "./reconciliation-sql" + +export type RefKeyParts = { + readonly graph: GraphKind + readonly entityId: string + readonly revision: string +} + +export type Outcome = + | "legacy_unavailable_projection_consistent" + | "legacy_unavailable_projection_duplicate_refs" + | "legacy_match" + | "legacy_mismatch" + +export type DiffSummary = { + readonly mode: "projection_internal" | "legacy_vs_projection" + readonly refCount: number + readonly duplicateCount: number + readonly perGraph: Readonly> + readonly legacy?: { + readonly refCount: number + readonly onlyInLegacy: readonly string[] + readonly onlyInProjection: readonly string[] + readonly sharedCount: number + } + readonly note?: string +} + +export type RecordInput = { + readonly sessionId: string + readonly activityId: string + readonly turnReceiptId: string + readonly selectionId?: string + readonly projectionRefs: readonly RefKeyParts[] + readonly legacyRefs?: readonly RefKeyParts[] + readonly now?: number +} + +export function refKey(parts: RefKeyParts): string { + return `${parts.graph}:${parts.entityId}:${parts.revision}` +} + +/** Sorted, de-duplicated ref keys — the canonical set representation for fingerprinting/diffing. */ +export function refKeys(refs: readonly RefKeyParts[]): readonly string[] { + return [...new Set(refs.map(refKey))].sort() +} + +export function duplicateCount(refs: readonly RefKeyParts[]): number { + return refs.length - new Set(refs.map(refKey)).size +} + +export function refsFingerprint(refs: readonly RefKeyParts[]): string { + return Hash.sha256(JSON.stringify(refKeys(refs))) +} + +export function diffRefKeys(a: readonly string[], b: readonly string[]) { + const setA = new Set(a) + const setB = new Set(b) + return { + onlyInA: a.filter((key) => !setB.has(key)), + onlyInB: b.filter((key) => !setA.has(key)), + sharedCount: a.filter((key) => setB.has(key)).length, + } +} + +function perGraph(refs: readonly RefKeyParts[]): Record { + return refs.reduce>( + (counts, ref) => ({ ...counts, [ref.graph]: (counts[ref.graph] ?? 0) + 1 }), + {}, + ) +} + +export function evaluate(input: { + readonly projectionRefs: readonly RefKeyParts[] + readonly legacyRefs?: readonly RefKeyParts[] +}): { + readonly outcome: Outcome + readonly projectionFingerprint: string + readonly legacyFingerprint?: string + readonly diffSummary: DiffSummary +} { + const projectionFingerprint = refsFingerprint(input.projectionRefs) + const duplicates = duplicateCount(input.projectionRefs) + const base = { + refCount: input.projectionRefs.length, + duplicateCount: duplicates, + perGraph: perGraph(input.projectionRefs), + } + if (!input.legacyRefs) { + // Legacy selected refs are produced inside the provider prepare path only; the receipt + // write site has the projection selection exclusively. Record projection-internal + // consistency instead of a cross-path diff (FEAT-007 fallback). + return { + outcome: duplicates > 0 ? "legacy_unavailable_projection_duplicate_refs" : "legacy_unavailable_projection_consistent", + projectionFingerprint, + diffSummary: { + ...base, + mode: "projection_internal", + note: "legacy selected refs unavailable at receipt write site; projection-internal consistency recorded", + }, + } + } + const legacyKeys = refKeys(input.legacyRefs) + const projectionKeys = refKeys(input.projectionRefs) + const diff = diffRefKeys(legacyKeys, projectionKeys) + return { + outcome: diff.onlyInA.length === 0 && diff.onlyInB.length === 0 ? "legacy_match" : "legacy_mismatch", + projectionFingerprint, + legacyFingerprint: Hash.sha256(JSON.stringify(legacyKeys)), + diffSummary: { + ...base, + mode: "legacy_vs_projection", + legacy: { + refCount: input.legacyRefs.length, + onlyInLegacy: diff.onlyInA, + onlyInProjection: diff.onlyInB, + sharedCount: diff.sharedCount, + }, + }, + } +} + +/** Deterministic per-receipt identity; safe for idempotent retries. */ +export function reconciliationId(turnReceiptId: string): string { + return Hash.sha256(`session-context-reconciliation:${turnReceiptId}`) +} + +/** + * Durable per-turn reconciliation write. Idempotent on `turn_receipt_id`; callers are + * expected to wrap this best-effort so a failure never blocks the turn. + */ +export function record(db: Database.Interface["db"], input: RecordInput) { + return Effect.gen(function* () { + const evaluated = evaluate(input) + yield* db + .insert(SessionContextReconciliationTable) + .values({ + reconciliation_id: reconciliationId(input.turnReceiptId), + session_id: input.sessionId, + activity_id: input.activityId, + turn_receipt_id: input.turnReceiptId, + selection_id: input.selectionId ?? null, + legacy_refs_fingerprint: evaluated.legacyFingerprint ?? null, + projection_refs_fingerprint: evaluated.projectionFingerprint, + outcome: evaluated.outcome, + diff_summary: evaluated.diffSummary, + created_at: input.now ?? Date.now(), + }) + .onConflictDoNothing({ target: SessionContextReconciliationTable.turn_receipt_id }) + return evaluated + }) +} diff --git a/packages/core/src/context-federation/session-context.ts b/packages/core/src/context-federation/session-context.ts index 652cbdc3..dda3b106 100644 --- a/packages/core/src/context-federation/session-context.ts +++ b/packages/core/src/context-federation/session-context.ts @@ -187,7 +187,7 @@ export interface Interface { readonly activityId: string readonly state: "settled" | "failed" | "interrupted" readonly now?: number - }) => Effect.Effect + }) => Effect.Effect readonly commitSelection: (input: CommitSelectionInput) => Effect.Effect readonly appendValidation: (input: ValidationInput) => Effect.Effect readonly hasValidValidation: (input: { @@ -426,12 +426,16 @@ export const layer = Layer.effect( return yield* db .transaction((tx) => Effect.gen(function* () { + // Idempotent settle (BUG-003): a missing row or one already in a terminal state is a + // no-op, not an error — multiple terminal paths (runLoop exit, resolve rollback, + // restart recovery) may legitimately race to settle the same activity. Real DB + // failures still surface through the error channel. const current = yield* tx .select() .from(SessionActivityTable) .where(eq(SessionActivityTable.activity_id, input.activityId)) .get() - if (!current || current.state !== "active") return yield* new ActivityStateError() + if (!current || current.state !== "active") return undefined const settled = yield* tx .update(SessionActivityTable) .set({ state: input.state, settled_at: input.now ?? Date.now() }) @@ -440,7 +444,7 @@ export const layer = Layer.effect( ) .returning() .get() - if (!settled) return yield* new ActivityStateError() + if (!settled) return undefined return activity(settled) }), ) diff --git a/packages/core/src/database/database.ts b/packages/core/src/database/database.ts index edc3acb7..c41db1d4 100644 --- a/packages/core/src/database/database.ts +++ b/packages/core/src/database/database.ts @@ -14,6 +14,13 @@ import { containsDataPath } from "../global-path" const makeDatabase = EffectDrizzleSqlite.makeWithDefaults() type DatabaseShape = Effect.Success +// Compatibility protocol this runtime implements. Protocol 2 is the 1.4.5/1.4.6-era; protocol 3 +// marks databases that carry provider-owner successor generations (BUG-407-009 recovery fence): an +// older binary that does not understand successor tokens must refuse to open such a database, or its +// startup recovery would re-quarantine the successor state. +export const SupportedReaderProtocol = 3 +export const SupportedWriterProtocol = 3 + export interface Interface { db: DatabaseShape } @@ -43,10 +50,10 @@ export const layer = Layer.effect( minimum_writer_protocol: number }>("SELECT capability, minimum_reader_protocol, minimum_writer_protocol FROM database_capability") for (const capability of capabilities) { - if (capability.minimum_reader_protocol > 2 || capability.minimum_writer_protocol > 2) + if (capability.minimum_reader_protocol > SupportedReaderProtocol || capability.minimum_writer_protocol > SupportedWriterProtocol) return yield* Effect.die( new Error( - `Database capability ${capability.capability} requires reader protocol ${capability.minimum_reader_protocol} and writer protocol ${capability.minimum_writer_protocol}; this runtime supports protocol 2`, + `Database capability ${capability.capability} requires reader protocol ${capability.minimum_reader_protocol} and writer protocol ${capability.minimum_writer_protocol}; this runtime supports protocol ${SupportedWriterProtocol}`, ), ) } diff --git a/packages/core/src/database/migration.gen.ts b/packages/core/src/database/migration.gen.ts index 2029c194..4e3c0e3d 100644 --- a/packages/core/src/database/migration.gen.ts +++ b/packages/core/src/database/migration.gen.ts @@ -139,5 +139,15 @@ export const migrations = ( import("./migration/20260813121129_v2_provider_parity_response_fingerprint"), import("./migration/20260813121200_v2_provider_parity_campaign_authority"), import("./migration/20260813150000_single_authority_snapshot_merge"), + import("./migration/20260815120000_event_delivery_claim_lease"), + import("./migration/20260815130000_event_multiprocess_extensions"), + import("./migration/20260816000000_handoff_admission_receipt"), + import("./migration/20260816073717_session_facade_activity"), + import("./migration/20260816080000_receipt_active_pack_set_attribution"), + import("./migration/20260816081000_session_context_reconciliation"), + import("./migration/20260819120000_session_intent_confirmation_state"), + import("./migration/20260820000000_remote_compact_persistence"), + import("./migration/20260820120000_session_turn_stage_evidence"), + import("./migration/20260820130000_compaction_continuation_fail_closed"), ]) ).map((module) => module.default) satisfies DatabaseMigration.Migration[] diff --git a/packages/core/src/database/migration.ts b/packages/core/src/database/migration.ts index cf9028dc..b4a044d7 100644 --- a/packages/core/src/database/migration.ts +++ b/packages/core/src/database/migration.ts @@ -1,13 +1,33 @@ export * as DatabaseMigration from "./migration" import { sql } from "drizzle-orm" -import { Effect, Semaphore } from "effect" +import { Cause, Data, Effect, Exit, Schedule, Semaphore } from "effect" import type { EffectDrizzleSqlite } from "@deepagent-code/effect-drizzle-sqlite" import { migrations } from "./migration.gen" type Database = EffectDrizzleSqlite.EffectSQLiteDatabase type Transaction = Parameters[0]>[0] const lock = Semaphore.makeUnsafe(1) + +// A migration DDL can fail transiently when another connection (a second desktop instance, a +// lingering daemon, or an in-process worker) holds an active statement against the old schema: +// SQLite returns SQLITE_LOCKED immediately and busy_timeout does not apply to schema locks. +// The migration transaction rolls back cleanly, so retrying once the competing connection +// releases is safe; without the retry a desktop startup dies on a transient race. +class TransientLock extends Data.TaggedError("DatabaseMigration.TransientLock")<{ readonly detail: string }> {} + +const isLockError = (value: unknown): boolean => { + if (typeof value !== "object" || value === null) return false + const record = value as { code?: string; message?: string; cause?: unknown } + if (record.code === "SQLITE_LOCKED" || record.code === "SQLITE_BUSY") return true + if (typeof record.message === "string" && /(database is locked|database table is locked|database is busy)/i.test(record.message)) + return true + return record.cause !== undefined ? isLockError(record.cause) : false +} + +const isTransientLockCause = (cause: Cause.Cause): boolean => cause.reasons.some(isLockError) + +const transientLockRetry = Schedule.spaced("2 seconds").pipe(Schedule.take(30)) const historicalAliases = new Map([ ["20260530232709_lovely_romulus", "20260511173437_session-metadata"], ["20260803000000_subagent_control_plane_l1", "20260803000001_subagent_control_plane_l1"], @@ -21,6 +41,9 @@ const mergedHistoryInsertions = new Set([ "20260813100000_event_snapshot_authority", "20260813110000_provider_recovery_authority_bridge", "20260813120000_legacy_provider_receipt_supersession", + "20260813120346_v2_provider_parity_campaign", + "20260813121129_v2_provider_parity_response_fingerprint", + "20260813121200_v2_provider_parity_campaign_authority", "20260813125000_event_sync_backfill_authority", "20260813130000_file_part_artifact", "20260813131000_event_snapshot_chunks", @@ -32,6 +55,7 @@ const mergedHistoryInsertions = new Set([ "20260813141000_bug_407_010_sidecar_lifecycle", "20260813142000_bug_407_010_sidecar_indexes", "20260813143000_bug_407_010_aggregate_indexes", + "20260813150000_single_authority_snapshot_merge", ]) export type Migration = { @@ -109,18 +133,39 @@ function applyMigrations(db: Database, input: Migration[], requireLinearHistory: for (const migration of input) { if (completed.has(migration.id)) continue - yield* db.transaction((tx) => - Effect.gen(function* () { - if (!process.env.DEEPAGENT_CODE_SKIP_MIGRATIONS) { - if (reconcileMergedHistory && migration.id === "20260812120000_legacy_provider_recovery") - yield* reconcileLegacyProviderRecovery(tx) - else yield* migration.up(tx) - } - yield* tx.run( - sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`, - ) - }), - ) + yield* db + .transaction((tx) => + Effect.gen(function* () { + if (!process.env.DEEPAGENT_CODE_SKIP_MIGRATIONS) { + if (reconcileMergedHistory && migration.id === "20260812120000_legacy_provider_recovery") + yield* reconcileLegacyProviderRecovery(tx) + else yield* migration.up(tx) + } + yield* tx.run( + sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`, + ) + }), + ) + .pipe( + Effect.exit, + Effect.flatMap((exit) => { + if (Exit.isSuccess(exit)) return Effect.succeed(undefined) + if (isTransientLockCause(exit.cause)) return Effect.fail(new TransientLock({ detail: migration.id })) + return Effect.failCause(exit.cause) + }), + Effect.retry({ while: (error) => error instanceof TransientLock, schedule: transientLockRetry }), + Effect.exit, + Effect.flatMap((exit) => { + if (Exit.isSuccess(exit)) return Effect.succeed(undefined) + if (exit.cause.reasons.some((reason) => reason instanceof TransientLock)) + return Effect.die( + new Error( + `database migration ${migration.id} was blocked by another connection for 60s; close other DeepAgent Code windows and restart`, + ), + ) + return Effect.failCause(exit.cause) + }), + ) } }) } diff --git a/packages/core/src/database/migration/20260815120000_event_delivery_claim_lease.ts b/packages/core/src/database/migration/20260815120000_event_delivery_claim_lease.ts new file mode 100644 index 00000000..bcfe1694 --- /dev/null +++ b/packages/core/src/database/migration/20260815120000_event_delivery_claim_lease.ts @@ -0,0 +1,22 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +// RISK-001: claim/lease columns for durable event retry delivery. `dueRetries()` was an unlocked +// global scan — two processes (Desktop + CLI daemon, or an upgrade overlap) could pick up the same +// pending row and execute its side effects twice. A claimant now atomically stamps a token + lease; +// rows whose lease expired become eligible for re-claim. Nullable so pre-existing rows stay valid. +export default { + id: "20260815120000_event_delivery_claim_lease", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE deepagent_event_delivery ADD COLUMN claim_token TEXT`) + yield* tx.run(`ALTER TABLE deepagent_event_delivery ADD COLUMN claimant_id TEXT`) + yield* tx.run(`ALTER TABLE deepagent_event_delivery ADD COLUMN claimed_at INTEGER`) + yield* tx.run(`ALTER TABLE deepagent_event_delivery ADD COLUMN lease_expires_at INTEGER`) + yield* tx.run(` + CREATE INDEX deepagent_event_delivery_claim_idx + ON deepagent_event_delivery (subscription_group, status, next_attempt_at) + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260815130000_event_multiprocess_extensions.ts b/packages/core/src/database/migration/20260815130000_event_multiprocess_extensions.ts new file mode 100644 index 00000000..26d4108d --- /dev/null +++ b/packages/core/src/database/migration/20260815130000_event_multiprocess_extensions.ts @@ -0,0 +1,41 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +// RISK-006: multi-process extensions for the EventV2 bus. +// +// 1. `deepagent_consumer_group.workspace_id` — durable consumer groups had no tenant field, so a +// group registry could not express which workspace a group consumes for. Default '' keeps +// pre-existing (global/unscoped) registrations valid. +// 2. `deepagent_event_delivery.priority` — denormalized event priority so `dueRetries`/`claimDue` +// can order high-priority work first WITHOUT joining the event table. Nullable: pre-existing +// rows read as NULL and are treated as "normal" by the ordering. +// 3. `deepagent_rate_limit_bucket` — durable fixed-window buckets for the tryPublish rate gate. +// Previously the limiter was an in-process Map, so N cooperating processes each enforced their +// own 1000/min/workspace quota (×N total). The bucket now lives in the shared DB and is +// updated inside an immediate transaction, so all processes enforce ONE combined quota. +export default { + id: "20260815130000_event_multiprocess_extensions", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`ALTER TABLE deepagent_consumer_group ADD COLUMN workspace_id TEXT NOT NULL DEFAULT ''`) + yield* tx.run(`ALTER TABLE deepagent_event_delivery ADD COLUMN priority TEXT`) + yield* tx.run(` + CREATE TABLE IF NOT EXISTS deepagent_rate_limit_bucket ( + workspace_id TEXT NOT NULL, + window_start INTEGER NOT NULL, + count INTEGER NOT NULL, + PRIMARY KEY (workspace_id, window_start) + ) + `) + yield* tx.run(` + CREATE INDEX IF NOT EXISTS deepagent_consumer_group_workspace_idx + ON deepagent_consumer_group (workspace_id) + `) + // sweepPublishLimiter prunes elapsed windows by window_start alone (cross-workspace). + yield* tx.run(` + CREATE INDEX IF NOT EXISTS deepagent_rate_limit_bucket_window_idx + ON deepagent_rate_limit_bucket (window_start) + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260816000000_handoff_admission_receipt.ts b/packages/core/src/database/migration/20260816000000_handoff_admission_receipt.ts new file mode 100644 index 00000000..51472218 --- /dev/null +++ b/packages/core/src/database/migration/20260816000000_handoff_admission_receipt.ts @@ -0,0 +1,35 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +// FEAT-008 — durable admission receipt for `agent.handoff.requested` (see handoff-admission-sql.ts +// for the carrier decision: a NEW table, because deepagent_event_delivery models delivery lifecycle +// only, task_admission is the subagent task_run receipt, and agent_execution never models the +// consumer's processing/terminal admission). Forward-only: pure additive CREATE TABLE + indexes. +export default { + id: "20260816000000_handoff_admission_receipt", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE IF NOT EXISTS \`deepagent_handoff_admission\` ( + \`handoff_id\` text PRIMARY KEY NOT NULL, + \`event_id\` text NOT NULL, + \`workspace_id\` text NOT NULL, + \`state\` text NOT NULL, + \`claimant_id\` text NOT NULL, + \`reason\` text, + \`started_at\` integer NOT NULL, + \`updated_at\` integer NOT NULL, + \`settled_at\` integer + ); + `) + yield* tx.run(` + CREATE INDEX IF NOT EXISTS \`deepagent_handoff_admission_workspace_state_idx\` + ON \`deepagent_handoff_admission\` (\`workspace_id\`, \`state\`); + `) + yield* tx.run(` + CREATE UNIQUE INDEX IF NOT EXISTS \`deepagent_handoff_admission_event_idx\` + ON \`deepagent_handoff_admission\` (\`event_id\`); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260816073717_session_facade_activity.ts b/packages/core/src/database/migration/20260816073717_session_facade_activity.ts new file mode 100644 index 00000000..9ac5cc91 --- /dev/null +++ b/packages/core/src/database/migration/20260816073717_session_facade_activity.ts @@ -0,0 +1,83 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +// FEAT-011 T1 — facade activity authority schema. Forward-only and purely additive: creates +// the new `session_facade_activity` base table and nothing else. The federation objective / +// permission child tables keep their ('legacy', 'v2') CHECKs on purpose — they are the fence +// that keeps the facade branch isolated from the objective/permission machinery (facade +// settlement is raw-SQL CAS on the base table, mirroring the legacy branch). +// The drizzle-generated CREATE TABLE/index statements below are augmented with CHECK clauses +// and lifecycle triggers that drizzle cannot express. +export default { + id: "20260816073717_session_facade_activity", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE \`session_facade_activity\` ( + \`activity_id\` text PRIMARY KEY, + \`subkind\` text NOT NULL CHECK (\`subkind\` IN ('task', 'goal', 'panel')), + \`parent_session_id\` text NOT NULL, + \`owner_session_id\` text, + \`spawn_tool_call_id\` text, + \`objective_text\` text, + \`budget_json\` text, + \`state\` text NOT NULL CHECK (\`state\` IN ('active', 'settled', 'failed', 'interrupted', 'recovery_required')), + \`reason_code\` text, + \`source\` text, + \`created_at\` integer NOT NULL, + \`settled_at\` integer, + \`mutation_epoch\` integer NOT NULL CHECK (\`mutation_epoch\` >= 0), + CONSTRAINT \`fk_session_facade_activity_parent_session_id_session_id_fk\` FOREIGN KEY (\`parent_session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE, + CONSTRAINT \`fk_session_facade_activity_owner_session_id_session_id_fk\` FOREIGN KEY (\`owner_session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE, + CHECK ( + (\`state\` = 'active' AND \`settled_at\` IS NULL AND \`reason_code\` IS NULL) OR + (\`state\` != 'active' AND \`settled_at\` IS NOT NULL AND \`reason_code\` IS NOT NULL) + ) + ); + `) + yield* tx.run(`CREATE UNIQUE INDEX \`session_facade_activity_active_idx\` ON \`session_facade_activity\` (\`parent_session_id\`,\`subkind\`) WHERE "session_facade_activity"."state" = 'active';`) + yield* tx.run(`CREATE INDEX \`session_facade_activity_parent_idx\` ON \`session_facade_activity\` (\`parent_session_id\`,\`state\`,\`created_at\`);`) + + // BUG-004 shape: opening a new facade activity requires settling the previous active one + // of the same parent+subkind first — enforced by the partial unique index above plus the + // single-shot active→terminal transition rule below (mirrors session_legacy_activity). + yield* tx.run(` + CREATE TRIGGER session_facade_activity_legal_update + BEFORE UPDATE ON session_facade_activity + WHEN NEW.activity_id != OLD.activity_id + OR NEW.subkind != OLD.subkind + OR NEW.parent_session_id != OLD.parent_session_id + OR COALESCE(NEW.owner_session_id, '') != COALESCE(OLD.owner_session_id, '') + OR COALESCE(NEW.spawn_tool_call_id, '') != COALESCE(OLD.spawn_tool_call_id, '') + OR COALESCE(NEW.objective_text, '') != COALESCE(OLD.objective_text, '') + OR COALESCE(NEW.budget_json, '') != COALESCE(OLD.budget_json, '') + OR COALESCE(NEW.source, '') != COALESCE(OLD.source, '') + OR NEW.created_at != OLD.created_at + OR OLD.state != 'active' + OR NEW.state NOT IN ('settled', 'failed', 'interrupted', 'recovery_required') + OR NEW.settled_at IS NULL + OR NEW.reason_code IS NULL + BEGIN + SELECT RAISE(ABORT, 'illegal session_facade_activity transition'); + END + `) + yield* tx.run(` + CREATE TRIGGER session_facade_activity_permission_effect_terminal_guard + BEFORE UPDATE OF state ON session_facade_activity + WHEN NEW.state IN ('settled', 'failed', 'interrupted', 'recovery_required') AND EXISTS ( + SELECT 1 + FROM session_activity_permission_effect_dispatch dispatch + WHERE dispatch.activity_kind = 'facade' + AND dispatch.activity_id = OLD.activity_id + AND ( + dispatch.state = 'started' OR + (NEW.state IN ('settled', 'interrupted') AND dispatch.state = 'unknown') + ) + ) + BEGIN + SELECT RAISE(ABORT, 'activity has unresolved permission effects'); + END + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260816080000_receipt_active_pack_set_attribution.ts b/packages/core/src/database/migration/20260816080000_receipt_active_pack_set_attribution.ts new file mode 100644 index 00000000..70c81196 --- /dev/null +++ b/packages/core/src/database/migration/20260816080000_receipt_active_pack_set_attribution.ts @@ -0,0 +1,24 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +// FEAT-007 — attribution column for the active_pack_set snapshot locked at the run entry. +// The receipt table lives outside the core drizzle schema (typed in deepagent-code only), so +// this stays a hand-authored forward-only ALTER, mirroring the released_knowledge_turn_binding +// precedent. NULL is allowed: the federation resolve result does not expose the pack snapshot +// id, so the receipt write site records NULL until it does (never blocks admission). +export default { + id: "20260816080000_receipt_active_pack_set_attribution", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + ALTER TABLE session_tool_request_receipt ADD COLUMN context_active_pack_set_snapshot_id TEXT + CHECK (context_active_pack_set_snapshot_id IS NULL OR context_active_pack_set_snapshot_id LIKE 'pack_snapshot:%') + `) + yield* tx.run(` + CREATE INDEX session_tool_request_receipt_active_pack_set_idx + ON session_tool_request_receipt (context_active_pack_set_snapshot_id) + WHERE context_active_pack_set_snapshot_id IS NOT NULL + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260816081000_session_context_reconciliation.ts b/packages/core/src/database/migration/20260816081000_session_context_reconciliation.ts new file mode 100644 index 00000000..31e630c4 --- /dev/null +++ b/packages/core/src/database/migration/20260816081000_session_context_reconciliation.ts @@ -0,0 +1,49 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +// FEAT-007 — durable per-turn reconciliation of federated context selections. The in-memory +// knowledgeMemoryDelta was the only legacy-vs-projection evidence and evaporated on restart; +// this table persists one row per federated provider turn receipt. Forward-only: pure additive +// CREATE TABLE + indexes (hand-authored, mirroring the handoff_admission_receipt precedent — +// the table is typed in reconciliation-sql.ts but stays outside the drizzle-managed schema). +export default { + id: "20260816081000_session_context_reconciliation", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE IF NOT EXISTS \`session_context_reconciliation\` ( + \`reconciliation_id\` text PRIMARY KEY NOT NULL, + \`session_id\` text NOT NULL REFERENCES \`session\`(\`id\`) ON DELETE CASCADE, + \`activity_id\` text NOT NULL REFERENCES \`session_activity\`(\`activity_id\`) ON DELETE CASCADE, + \`turn_receipt_id\` text NOT NULL, + \`selection_id\` text, + \`legacy_refs_fingerprint\` text + CHECK (\`legacy_refs_fingerprint\` IS NULL OR ( + length(\`legacy_refs_fingerprint\`) = 64 AND + \`legacy_refs_fingerprint\` NOT GLOB '*[^0-9a-f]*' + )), + \`projection_refs_fingerprint\` text NOT NULL + CHECK ( + length(\`projection_refs_fingerprint\`) = 64 AND + \`projection_refs_fingerprint\` NOT GLOB '*[^0-9a-f]*' + ), + \`outcome\` text NOT NULL, + \`diff_summary\` text NOT NULL + CHECK ( + json_valid(\`diff_summary\`) = 1 AND + json_type(\`diff_summary\`) = 'object' + ), + \`created_at\` integer NOT NULL + ); + `) + yield* tx.run(` + CREATE UNIQUE INDEX IF NOT EXISTS \`session_context_reconciliation_receipt_idx\` + ON \`session_context_reconciliation\` (\`turn_receipt_id\`); + `) + yield* tx.run(` + CREATE INDEX IF NOT EXISTS \`session_context_reconciliation_session_idx\` + ON \`session_context_reconciliation\` (\`session_id\`, \`created_at\`); + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260819120000_session_intent_confirmation_state.ts b/packages/core/src/database/migration/20260819120000_session_intent_confirmation_state.ts new file mode 100644 index 00000000..7dbca4cd --- /dev/null +++ b/packages/core/src/database/migration/20260819120000_session_intent_confirmation_state.ts @@ -0,0 +1,253 @@ +/** + * BUG-405-003 residual P1: fold `awaiting_confirmation` and `selected` into the durable + * session_intent state contract (docs/bug-405-003.md §12.1). + * + * state = preparing | awaiting_confirmation | selected | admitting | + * admitted | canceled | superseded | failed + * + * SQLite cannot alter a CHECK constraint in place, so session_intent must be rebuilt. + * session_activity_admission.legacy_intent_id references session_intent, and with + * PRAGMA foreign_keys enabled (runtime connection) dropping the parent table performs an + * implicit DELETE that fails on the referencing admission rows. The whole legacy activity + * chain (admission -> legacy_activity -> {admission membership, progress, run, terminal, + * migration receipt}) is therefore detached into temp backups first — the same strategy + * used by 20260812120000_legacy_provider_recovery — then rebuilt and restored verbatim. + * + * The detached tables are recreated from their captured sqlite_master definitions (not + * hardcoded final shapes) because merged-lineage histories can reach this migration before + * 20260812120000/20260812130000 are applied; later authorities must still find the exact + * shapes they expect to ALTER or rebuild. Triggers are recreated after the data restore so + * restore statements cannot fire validation or projection side effects (e.g. duplicate + * session_activity_objective rows). + */ + +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +const LEGACY_STATE_CHECK = + "CHECK (state IN ('preparing', 'admitting', 'admitted', 'canceled', 'superseded', 'failed'))" +const WIDENED_STATE_CHECK = + "CHECK (state IN ('preparing', 'awaiting_confirmation', 'selected', 'admitting', 'admitted', 'canceled', 'superseded', 'failed'))" + +export default { + id: "20260819120000_session_intent_confirmation_state", + up(tx) { + return Effect.gen(function* () { + const has = (table: string) => + Effect.gen(function* () { + return Boolean( + yield* tx.get(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = '${table}'`), + ) + }) + const ddlOf = (type: string, name: string) => + Effect.gen(function* () { + const row = yield* tx.get<{ sql: string }>( + `SELECT sql FROM sqlite_master WHERE type = '${type}' AND name = '${name}'`, + ) + return row!.sql + }) + + const hasAdmission = yield* has("session_activity_admission") + const hasLegacyActivity = yield* has("session_legacy_activity") + const hasAdmissionJoin = yield* has("session_legacy_activity_admission") + const hasProgress = yield* has("session_activity_progress") + const hasRun = yield* has("session_legacy_activity_run") + const hasTerminal = yield* has("session_legacy_activity_terminal") + const hasMigrationReceipt = yield* has("session_legacy_activity_migration_receipt") + + // Merged-lineage histories may not have applied the execution-identity expansion yet. + const intentColumns = yield* tx.all<{ name: string }>(`PRAGMA table_info(session_intent)`) + const hasExecutionColumns = intentColumns.some((column) => column.name === "execution_mode") + const intentDDL = yield* ddlOf("table", "session_intent") + const intentIndexes = yield* tx.all<{ sql: string }>(` + SELECT sql FROM sqlite_master + WHERE type = 'index' AND tbl_name = 'session_intent' + AND sql IS NOT NULL AND name NOT LIKE 'sqlite_%' + `) + + // Captured current shapes of the detached chain (recreated verbatim later). + const chainTables: Array<{ name: string; sql: string }> = [] + if (hasAdmission) chainTables.push({ name: "session_activity_admission", sql: yield* ddlOf("table", "session_activity_admission") }) + if (hasLegacyActivity) chainTables.push({ name: "session_legacy_activity", sql: yield* ddlOf("table", "session_legacy_activity") }) + if (hasAdmissionJoin) chainTables.push({ name: "session_legacy_activity_admission", sql: yield* ddlOf("table", "session_legacy_activity_admission") }) + if (hasProgress) chainTables.push({ name: "session_activity_progress", sql: yield* ddlOf("table", "session_activity_progress") }) + if (hasRun) chainTables.push({ name: "session_legacy_activity_run", sql: yield* ddlOf("table", "session_legacy_activity_run") }) + if (hasTerminal) chainTables.push({ name: "session_legacy_activity_terminal", sql: yield* ddlOf("table", "session_legacy_activity_terminal") }) + if (hasMigrationReceipt) chainTables.push({ name: "session_legacy_activity_migration_receipt", sql: yield* ddlOf("table", "session_legacy_activity_migration_receipt") }) + const chainTableNames = chainTables.map((table) => `'${table.name}'`).join(", ") + const chainIndexes = chainTableNames.length + ? yield* tx.all<{ tbl_name: string; sql: string }>(` + SELECT tbl_name, sql FROM sqlite_master + WHERE type = 'index' AND tbl_name IN (${chainTableNames}) + AND sql IS NOT NULL AND name NOT LIKE 'sqlite_%' + `) + : [] + const chainTriggers = chainTableNames.length + ? yield* tx.all<{ sql: string }>(` + SELECT sql FROM sqlite_master + WHERE type = 'trigger' AND tbl_name IN (${chainTableNames}) AND sql IS NOT NULL + `) + : [] + + // ── Detach the FK children of session_intent into transaction-local backups ── + if (hasMigrationReceipt) + yield* tx.run(` + CREATE TEMP TABLE intent_state_backup_migration_receipt AS + SELECT * FROM session_legacy_activity_migration_receipt + `) + if (hasTerminal) + yield* tx.run(` + CREATE TEMP TABLE intent_state_backup_terminal AS + SELECT * FROM session_legacy_activity_terminal + `) + if (hasRun) + yield* tx.run(` + CREATE TEMP TABLE intent_state_backup_run AS + SELECT * FROM session_legacy_activity_run + `) + if (hasAdmissionJoin) + yield* tx.run(` + CREATE TEMP TABLE intent_state_backup_admission_join AS + SELECT * FROM session_legacy_activity_admission + `) + if (hasProgress) + yield* tx.run(` + CREATE TEMP TABLE intent_state_backup_progress AS + SELECT * FROM session_activity_progress + `) + if (hasLegacyActivity) + yield* tx.run(` + CREATE TEMP TABLE intent_state_backup_activity AS + SELECT * FROM session_legacy_activity + `) + if (hasAdmission) + yield* tx.run(` + CREATE TEMP TABLE intent_state_backup_admission AS + SELECT * FROM session_activity_admission + `) + + // Children first so each drop only removes rows of the table itself. + if (hasMigrationReceipt) yield* tx.run(`DROP TABLE session_legacy_activity_migration_receipt`) + if (hasTerminal) yield* tx.run(`DROP TABLE session_legacy_activity_terminal`) + if (hasRun) yield* tx.run(`DROP TABLE session_legacy_activity_run`) + if (hasAdmissionJoin) yield* tx.run(`DROP TABLE session_legacy_activity_admission`) + if (hasProgress) yield* tx.run(`DROP TABLE session_activity_progress`) + if (hasLegacyActivity) yield* tx.run(`DROP TABLE session_legacy_activity`) + if (hasAdmission) yield* tx.run(`DROP TABLE session_activity_admission`) + + // ── Rebuild session_intent with the BUG-405-003 §12.1 state contract ── + // Only the state CHECK changes; every other column definition is preserved verbatim so + // histories lacking the execution-identity columns can still receive them later. + const rebuiltDDL = intentDDL + .replace("CREATE TABLE session_intent", "CREATE TABLE session_intent_rebuilt") + .replace(LEGACY_STATE_CHECK, WIDENED_STATE_CHECK) + yield* tx.run(rebuiltDDL) + const columnNames = intentColumns.map((column) => column.name).join(", ") + yield* tx.run(` + INSERT INTO session_intent_rebuilt (${columnNames}) + SELECT ${columnNames} FROM session_intent + `) + yield* tx.run(`DROP TABLE session_intent`) + // SQLite >= 3.25 rewrites referencing trigger/view bodies during RENAME when foreign_keys + // is ON; triggers on OTHER tables (e.g. session_tool_request_resolution_validate_insert) + // reference the still-dropped legacy chain at this point and make the rewrite fail + // ("no such table: main.session_activity_progress"). The rename target is the same name as + // the original table, so no reference rewrite is needed — use the legacy behavior for this + // single statement. The desktop runtime (node:sqlite, SQLite 3.51) hits this; bun:sqlite's + // older engine does not, which is why tests and bun-driven drills were green. + yield* tx.run(`PRAGMA legacy_alter_table = ON`) + yield* tx.run(`ALTER TABLE session_intent_rebuilt RENAME TO session_intent`) + yield* tx.run(`PRAGMA legacy_alter_table = OFF`) + for (const index of intentIndexes) yield* tx.run(index.sql) + if (hasExecutionColumns) { + yield* tx.run(` + CREATE TRIGGER session_intent_execution_validate_insert + BEFORE INSERT ON session_intent + WHEN NOT ( + (NEW.execution_mode = 'legacy' AND NEW.execution_state = 'legacy' + AND NEW.execution_claim_id IS NULL AND NEW.execution_claimed_at IS NULL) OR + (NEW.execution_mode IN ('run_now','deferred') AND NEW.execution_state = 'pending' + AND NEW.execution_claim_id IS NULL AND NEW.execution_claimed_at IS NULL) OR + (NEW.execution_mode IN ('run_now','deferred') AND NEW.execution_state = 'claimed' + AND NEW.execution_claim_id IS NOT NULL AND NEW.execution_claimed_at IS NOT NULL) OR + (NEW.execution_mode = 'deferred' AND NEW.execution_state = 'absorbed' + AND NEW.execution_claim_id IS NOT NULL AND NEW.execution_claimed_at IS NOT NULL) OR + (NEW.execution_mode IN ('run_now','deferred') AND NEW.execution_state = 'canceled' + AND NEW.execution_claim_id IS NULL AND NEW.execution_claimed_at IS NULL) + ) + BEGIN + SELECT RAISE(ABORT, 'invalid session intent execution identity'); + END + `) + yield* tx.run(` + CREATE TRIGGER session_intent_execution_validate_update + BEFORE UPDATE ON session_intent + WHEN NOT ( + (NEW.execution_mode = 'legacy' AND NEW.execution_state = 'legacy' + AND NEW.execution_claim_id IS NULL AND NEW.execution_claimed_at IS NULL) OR + (NEW.execution_mode IN ('run_now','deferred') AND NEW.execution_state = 'pending' + AND NEW.execution_claim_id IS NULL AND NEW.execution_claimed_at IS NULL) OR + (NEW.execution_mode IN ('run_now','deferred') AND NEW.execution_state = 'claimed' + AND NEW.execution_claim_id IS NOT NULL AND NEW.execution_claimed_at IS NOT NULL) OR + (NEW.execution_mode = 'deferred' AND NEW.execution_state = 'absorbed' + AND NEW.execution_claim_id IS NOT NULL AND NEW.execution_claimed_at IS NOT NULL) OR + (NEW.execution_mode IN ('run_now','deferred') AND NEW.execution_state = 'canceled' + AND NEW.execution_claim_id IS NULL AND NEW.execution_claimed_at IS NULL) + ) OR NOT ( + (NEW.execution_mode = OLD.execution_mode AND NEW.execution_state = OLD.execution_state + AND NEW.execution_claim_id IS OLD.execution_claim_id + AND NEW.execution_claimed_at IS OLD.execution_claimed_at) OR + (OLD.execution_state = 'pending' AND NEW.execution_mode = OLD.execution_mode + AND NEW.execution_state IN ('claimed','absorbed','canceled')) OR + (OLD.execution_mode = 'legacy' AND OLD.execution_state = 'legacy' + AND NEW.execution_mode IN ('run_now','deferred') + AND NEW.execution_state IN ('pending','claimed','absorbed','canceled')) + ) + BEGIN + SELECT RAISE(ABORT, 'illegal session intent execution transition'); + END + `) + } + + // ── Recreate the detached chain with its captured definitions, then restore data ── + // Each table's indexes are recreated immediately after the table itself: child tables + // resolve FK parents through explicit unique indexes (e.g. terminal.operation_id), so + // those indexes must exist before the referencing child CREATE TABLE is prepared. + // Restores are positional (SELECT *) because the captured shapes may differ between + // merged-lineage and full histories (e.g. execution_mode may not exist yet). + for (const table of chainTables) { + yield* tx.run(table.sql) + for (const index of chainIndexes) if (index.tbl_name === table.name) yield* tx.run(index.sql) + } + if (hasAdmission) + yield* tx.run(`INSERT INTO session_activity_admission SELECT * FROM intent_state_backup_admission`) + if (hasLegacyActivity) + yield* tx.run(`INSERT INTO session_legacy_activity SELECT * FROM intent_state_backup_activity`) + if (hasAdmissionJoin) + yield* tx.run(`INSERT INTO session_legacy_activity_admission SELECT * FROM intent_state_backup_admission_join`) + if (hasProgress) + yield* tx.run(`INSERT INTO session_activity_progress SELECT * FROM intent_state_backup_progress`) + if (hasRun) + yield* tx.run(`INSERT INTO session_legacy_activity_run SELECT * FROM intent_state_backup_run`) + if (hasTerminal) + yield* tx.run(`INSERT INTO session_legacy_activity_terminal SELECT * FROM intent_state_backup_terminal`) + if (hasMigrationReceipt) + yield* tx.run( + `INSERT INTO session_legacy_activity_migration_receipt SELECT * FROM intent_state_backup_migration_receipt`, + ) + + // ── Recreate triggers only after the data restore so no validation or projection + // side effects fire for rows that already existed before this migration ── + for (const trigger of chainTriggers) yield* tx.run(trigger.sql) + + // ── Drop the transaction-local backups ── + if (hasMigrationReceipt) yield* tx.run(`DROP TABLE intent_state_backup_migration_receipt`) + if (hasTerminal) yield* tx.run(`DROP TABLE intent_state_backup_terminal`) + if (hasRun) yield* tx.run(`DROP TABLE intent_state_backup_run`) + if (hasAdmissionJoin) yield* tx.run(`DROP TABLE intent_state_backup_admission_join`) + if (hasProgress) yield* tx.run(`DROP TABLE intent_state_backup_progress`) + if (hasLegacyActivity) yield* tx.run(`DROP TABLE intent_state_backup_activity`) + if (hasAdmission) yield* tx.run(`DROP TABLE intent_state_backup_admission`) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260820000000_remote_compact_persistence.ts b/packages/core/src/database/migration/20260820000000_remote_compact_persistence.ts new file mode 100644 index 00000000..3ce511f2 --- /dev/null +++ b/packages/core/src/database/migration/20260820000000_remote_compact_persistence.ts @@ -0,0 +1,86 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +// UPD-005 (server-side compaction, Gap 1 + Gap 2) — persistence for the opaque +// `encrypted_content` returned by `/responses/compact`, plus the mode columns on +// compaction_run. +// +// Gap 1: `session_compaction_encrypted_content` holds the ONE currently-valid blob +// per session (1:1, latest wins via ON CONFLICT(session_id) DO UPDATE at the write +// site). The blob is cross-run / session-scoped — it is sent back on the NEXT +// compaction so the provider can expand it — hence an independent table rather than +// a compaction_run column. `provider_id` is recorded so the read path can verify +// same-provenance before replaying a blob (a provider switch invalidates it). +// Cleanup: session deletion cascades; the fail-over path clears explicitly. +// +// Gap 2: compaction_run gains `compaction_mode` ('local_summary' — the default and +// the only value historical rows ever carry — | 'remote_compact') and +// `encrypted_content_session` (remote mode only: pointer to the blob row). Remote +// runs commit with `summary_text` NULL by design: losslessness is guaranteed by the +// provider's encrypted context, not by text, and the information-hole check exempts +// this mode. +// +// Forward-only: additive table + two ALTER ADD COLUMN statements; no existing data +// is touched. Existing compaction_run rows read back as 'local_summary'. +// +// The authority trigger is rebuilt (same name, same state machine) so a +// 'remote_compact' run may reach 'committed' without the TEXT-summary binding +// columns: losslessness for remote runs is guaranteed by the provider's encrypted +// context (see the information-hole exemption on the read path), not by +// summary_text. local_summary runs keep the exact previous commit binding check. +export default { + id: "20260820000000_remote_compact_persistence", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE session_compaction_encrypted_content ( + session_id TEXT NOT NULL PRIMARY KEY REFERENCES session(id) ON DELETE CASCADE, + encrypted_content TEXT NOT NULL, + provider_id TEXT NOT NULL, + model_id TEXT, + source_run_id TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + ) + `) + yield* tx.run(` + ALTER TABLE compaction_run ADD COLUMN compaction_mode TEXT NOT NULL DEFAULT 'local_summary' + CHECK (compaction_mode IN ('local_summary', 'remote_compact')) + `) + yield* tx.run(` + ALTER TABLE compaction_run ADD COLUMN encrypted_content_session TEXT + `) + // Rebuild the update authority trigger: identical transitions + // (requested → summarizing → committed|failed|indeterminate), but the commit + // binding check no longer applies to remote_compact runs, which commit with + // summary_text NULL and the encrypted_content pointer instead. + yield* tx.run("DROP TRIGGER compaction_run_authority_validate_update") + yield* tx.run(` + CREATE TRIGGER compaction_run_authority_validate_update + BEFORE UPDATE ON compaction_run + BEGIN + SELECT CASE WHEN NOT ( + NEW.state = OLD.state OR + (OLD.state = 'requested' AND NEW.state IN ('summarizing', 'failed', 'indeterminate')) OR + (OLD.state = 'summarizing' AND NEW.state IN ('committed', 'failed', 'indeterminate')) + ) THEN RAISE(ABORT, 'compaction_run_invalid_state_transition') END; + SELECT CASE WHEN NEW.state IN ('failed', 'indeterminate') AND NEW.terminal_failure_kind IS NULL + THEN RAISE(ABORT, 'compaction_run_terminal_failure_without_reason') END; + SELECT CASE WHEN OLD.state != 'committed' AND NEW.state = 'committed' + AND NEW.compaction_mode != 'remote_compact' + AND ( + NEW.target_prompt_epoch IS NULL OR NEW.committed_summary_message_id IS NULL OR + NEW.checkpoint_hash IS NULL OR NEW.summary_text IS NULL OR NEW.recent_context IS NULL OR + NEW.completion_reason IS NULL OR NEW.committed_at IS NULL + ) THEN RAISE(ABORT, 'compaction_run_commit_binding_incomplete') END; + SELECT CASE WHEN OLD.state != 'committed' AND NEW.state = 'committed' + AND NEW.compaction_mode = 'remote_compact' + AND ( + NEW.encrypted_content_session IS NULL OR + NEW.completion_reason IS NULL OR NEW.committed_at IS NULL + ) THEN RAISE(ABORT, 'compaction_run_commit_binding_incomplete') END; + END + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260820120000_session_turn_stage_evidence.ts b/packages/core/src/database/migration/20260820120000_session_turn_stage_evidence.ts new file mode 100644 index 00000000..1bc9b5bd --- /dev/null +++ b/packages/core/src/database/migration/20260820120000_session_turn_stage_evidence.ts @@ -0,0 +1,39 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +// BUG-407-012 gap C: durable per-turn stage evidence. +// One row per (session_id, activity_id), upserted forward-only as a provider turn +// crosses each boundary (claim -> snapshot -> history -> request -> dispatch -> settle). +// It exists so a turn stuck between "legacy activity claimed" and "provider receipt +// created" can be attributed post-hoc; it is observability evidence, NOT an authority. +export default { + id: "20260820120000_session_turn_stage_evidence", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(` + CREATE TABLE session_turn_stage_evidence ( + session_id TEXT NOT NULL REFERENCES session(id) ON DELETE CASCADE, + activity_id TEXT NOT NULL, + stage TEXT NOT NULL CHECK (stage IN ( + 'activity_claimed', + 'snapshot_started', + 'snapshot_finished', + 'snapshot_degraded', + 'history_loaded', + 'request_prepared', + 'provider_dispatch_started', + 'terminal_settled' + )), + details TEXT, -- JSON stage attribution (e.g. degraded reason, budget numbers) + stage_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (session_id, activity_id) + ) + `) + yield* tx.run( + `CREATE INDEX session_turn_stage_evidence_session_idx + ON session_turn_stage_evidence(session_id, updated_at)`, + ) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/database/migration/20260820130000_compaction_continuation_fail_closed.ts b/packages/core/src/database/migration/20260820130000_compaction_continuation_fail_closed.ts new file mode 100644 index 00000000..4b79bd60 --- /dev/null +++ b/packages/core/src/database/migration/20260820130000_compaction_continuation_fail_closed.ts @@ -0,0 +1,67 @@ +import { Effect } from "effect" +import type { DatabaseMigration } from "../migration" + +// BUG-407-009 §10.1 fail-closed path for committed compaction continuations whose recovery +// cannot proceed (e.g. the incident sessions have no assistant message for the v2 owner to +// continue from). The original continuation state machine only allowed pending → admitted, and +// the admission bindings (receipt id / admitted_at / response fingerprint) cannot exist for a +// run that never reached admission — so recovery failures could not be recorded, and instance +// initialization re-woke the same runs forever (log flood, endless retry loop). +// +// This migration widens the machine with one transition: pending → failed, exempt from the +// admission/response binding requirements. Real continuation recovery remains a Maintenance +// deliverable; this only makes the hotfix fail closed and stop re-advertising the run. + +export default { + id: "20260820130000_compaction_continuation_fail_closed", + up(tx) { + return Effect.gen(function* () { + yield* tx.run(`DROP TRIGGER compaction_run_continuation_transition`) + yield* tx.run(` + CREATE TRIGGER compaction_run_continuation_transition + BEFORE UPDATE OF continuation_state ON compaction_run + WHEN OLD.continuation_state IS NOT NEW.continuation_state AND NOT ( + (OLD.continuation_state IS NULL AND NEW.continuation_state = 'pending') OR + (OLD.continuation_state = 'pending' AND NEW.continuation_state IN ('admitted', 'failed')) OR + (OLD.continuation_state = 'admitted' AND NEW.continuation_state IN ('pending', 'dispatching', 'failed')) OR + (OLD.continuation_state = 'dispatching' AND NEW.continuation_state IN ('settled', 'failed', 'indeterminate')) + ) + BEGIN + SELECT RAISE(ABORT, 'illegal compaction continuation transition'); + END + `) + yield* tx.run(`DROP TRIGGER compaction_run_continuation_binding_validate`) + yield* tx.run(` + CREATE TRIGGER compaction_run_continuation_binding_validate + BEFORE UPDATE ON compaction_run + WHEN NOT (OLD.continuation_state = 'pending' AND NEW.continuation_state = 'failed') AND ( + (NEW.continuation_state IN ('admitted', 'dispatching', 'settled', 'failed', 'indeterminate') AND + (NEW.continuation_receipt_id IS NULL OR NEW.continuation_admitted_at IS NULL)) OR + (NEW.continuation_state IN ('dispatching', 'settled', 'failed', 'indeterminate') AND + NEW.continuation_dispatching_at IS NULL AND NEW.continuation_state != 'failed') OR + (NEW.continuation_state IN ('settled', 'failed', 'indeterminate') AND + NEW.continuation_terminal_at IS NULL) + ) + BEGIN + SELECT RAISE(ABORT, 'incomplete compaction continuation binding'); + END + `) + yield* tx.run(`DROP TRIGGER compaction_run_continuation_response_validate`) + yield* tx.run(` + CREATE TRIGGER compaction_run_continuation_response_validate + BEFORE UPDATE OF continuation_state ON compaction_run + WHEN NEW.continuation_state IN ('settled', 'failed') + AND NOT (OLD.continuation_state = 'pending' AND NEW.continuation_state = 'failed') + AND NOT EXISTS ( + SELECT 1 + FROM session_tool_request_receipt receipt + WHERE receipt.receipt_id = NEW.continuation_receipt_id + AND receipt.response_fingerprint IS NOT NULL + ) + BEGIN + SELECT RAISE(ABORT, 'compaction continuation response is not durable'); + END + `) + }) + }, +} satisfies DatabaseMigration.Migration diff --git a/packages/core/src/deepagent/activity-authority.sql.ts b/packages/core/src/deepagent/activity-authority.sql.ts index f522b63c..4e55bcae 100644 --- a/packages/core/src/deepagent/activity-authority.sql.ts +++ b/packages/core/src/deepagent/activity-authority.sql.ts @@ -6,7 +6,9 @@ import { SessionTable } from "../session/sql" import type { SessionSchema } from "../session/schema" import type { CompletionCriterion } from "./goal-loop" -export type ActivityKind = "legacy" | "v2" +export type ActivityKind = "legacy" | "v2" | "facade" +export type FacadeActivitySubkind = "task" | "goal" | "panel" +export type FacadeActivityState = "active" | "settled" | "failed" | "interrupted" | "recovery_required" export type ActivityObjectiveState = "active" | "completed" | "needs_human" | "interrupted" | "recovery_required" export type PermissionRequestState = | "pending" @@ -49,6 +51,40 @@ export const SessionActivityObjectiveTable = sqliteTable( ], ) +// FEAT-011 T1 — facade activity base table. Mirrors the session_legacy_activity terminal +// semantics (active carries no settled_at/reason_code; terminal states require both) and the +// v2 single-active invariant, scoped per parent session + subkind: opening a new facade +// activity requires settling the previous active one of the same subkind first (BUG-004). +export const SessionFacadeActivityTable = sqliteTable( + "session_facade_activity", + { + activity_id: text().primaryKey(), + subkind: text().$type().notNull(), + parent_session_id: text() + .$type() + .notNull() + .references(() => SessionTable.id, { onDelete: "cascade" }), + owner_session_id: text() + .$type() + .references(() => SessionTable.id, { onDelete: "cascade" }), + spawn_tool_call_id: text(), + objective_text: text(), + budget_json: text({ mode: "json" }).$type(), + state: text().$type().notNull(), + reason_code: text(), + source: text(), + created_at: integer().notNull(), + settled_at: integer(), + mutation_epoch: integer().notNull(), + }, + (table) => [ + uniqueIndex("session_facade_activity_active_idx") + .on(table.parent_session_id, table.subkind) + .where(sql`${table.state} = 'active'`), + index("session_facade_activity_parent_idx").on(table.parent_session_id, table.state, table.created_at), + ], +) + export const SessionActivityEvidenceTable = sqliteTable( "session_activity_evidence", { diff --git a/packages/core/src/deepagent/activity-authority.ts b/packages/core/src/deepagent/activity-authority.ts index 0203293c..37de3b97 100644 --- a/packages/core/src/deepagent/activity-authority.ts +++ b/packages/core/src/deepagent/activity-authority.ts @@ -23,6 +23,7 @@ import { SessionActivityPermissionOnceConsumptionTable, SessionActivityPermissionRequestTable, SessionActivityProgressObservationTable, + SessionFacadeActivityTable, } from "./activity-authority.sql" type DatabaseClient = Database.Interface["db"] @@ -119,7 +120,7 @@ export type Reconstructed = { } export class NotFoundError extends Schema.TaggedErrorClass()("ActivityAuthority.NotFoundError", { - activityKind: Schema.Literals(["legacy", "v2"]), + activityKind: Schema.Literals(["legacy", "v2", "facade"]), activityID: Schema.String, }) {} @@ -446,6 +447,11 @@ export const settle = Effect.fn("DeepAgentActivityAuthority.settle")(function* ( .transaction( (tx) => Effect.gen(function* () { + // FEAT-011 T2 — facade activities are fully isolated from the federation objective + // machinery (the objective/permission child tables keep ('legacy','v2') CHECKs), so + // settlement is raw CAS on the facade base table keyed by activity_id, mirroring the + // legacy branch shape. expectedVersion is the mutation_epoch CAS token. + if (input.activityKind === "facade") return yield* settleFacade(tx, input) const current = yield* tx.select().from(SessionActivityObjectiveTable).where(activityWhere(input)).get() if (!current) return yield* notFound(input) if ( @@ -539,6 +545,87 @@ export const settle = Effect.fn("DeepAgentActivityAuthority.settle")(function* ( .pipe(Effect.catchTag("EffectDrizzleQueryError", Effect.die), Effect.catchTag("SqlError", Effect.die)) }) +// FEAT-011 T2 — facade settlement path. Facade activities never own a federation objective +// row (the objective/permission child tables keep ('legacy','v2') CHECKs by design), so this +// settles the facade base table directly with a CAS keyed by activity_id. Three-state mapping +// mirrors the v2 branch: completed→'settled', interrupted→'interrupted', and +// recovery_required→'failed' — the facade base has no recovery_required shape of its own, so a +// recovery-required settlement is persisted as base state 'failed' (same modeling as the v2 +// federation base, where 'failed' means recovery is required). expectedVersion is the +// mutation_epoch CAS token; the base legal-update trigger additionally enforces the one-shot +// active→terminal transition with reason_code + settled_at. +function settleFacade( + tx: Transaction, + input: ActivityRef & { + readonly expectedVersion: number + readonly state: "completed" | "interrupted" | "recovery_required" + readonly terminalReason: string + }, +) { + return Effect.gen(function* () { + const current = yield* tx + .select() + .from(SessionFacadeActivityTable) + .where(eq(SessionFacadeActivityTable.activity_id, input.activityID)) + .get() + if (!current) return yield* notFound(input) + const baseState = input.state === "completed" ? "settled" : input.state === "recovery_required" ? "failed" : "interrupted" + const mapped = facadeObjective(current, input.state) + if (current.state === baseState && current.reason_code === input.terminalReason && current.settled_at !== null) + return mapped + if (current.mutation_epoch !== input.expectedVersion) + return yield* versionConflict(input.activityID, input.expectedVersion, current.mutation_epoch) + if (current.state !== "active") + return yield* new ConflictError({ entity: input.activityID, expected: "active", actual: current.state }) + const now = yield* observedAtInTransaction(tx) + const updated = yield* tx + .update(SessionFacadeActivityTable) + .set({ + state: baseState, + reason_code: input.terminalReason, + settled_at: now, + mutation_epoch: sql`${SessionFacadeActivityTable.mutation_epoch} + 1`, + }) + .where( + and( + eq(SessionFacadeActivityTable.activity_id, input.activityID), + eq(SessionFacadeActivityTable.state, "active"), + eq(SessionFacadeActivityTable.mutation_epoch, input.expectedVersion), + ), + ) + .returning() + .get() + if (!updated) return yield* versionConflict(input.activityID, input.expectedVersion, current.mutation_epoch) + return facadeObjective(updated, input.state) + }) +} + +// Projects a facade base row as the Objective shape settle callers expect (facade activities +// carry no objective row). version maps mutation_epoch; settlement state maps back from the +// base shape ('settled'→completed, 'failed'→recovery_required, otherwise as-is). +function facadeObjective( + row: typeof SessionFacadeActivityTable.$inferSelect, + state: ActivityObjectiveState, +): Objective { + return { + activityKind: "facade", + activityID: row.activity_id, + sessionID: row.parent_session_id, + version: row.mutation_epoch, + admissionFingerprint: "facade-spawn:" + (row.spawn_tool_call_id ?? row.activity_id), + ...(row.objective_text ? { objectiveText: row.objective_text } : {}), + completionCriteria: [], + enforcementState: "disabled", + state, + noProgressCount: 0, + latestObservationRevision: -1, + ...(row.reason_code ? { terminalReason: row.reason_code } : {}), + createdAt: row.created_at, + updatedAt: row.settled_at ?? row.created_at, + ...(row.settled_at !== null ? { settledAt: row.settled_at } : {}), + } +} + export const reconstruct = Effect.fn("DeepAgentActivityAuthority.reconstruct")(function* (input: ActivityRef) { const { db } = yield* Database.Service const row = yield* db @@ -812,11 +899,17 @@ const decidePermissionInternal = Effect.fn("DeepAgentActivityAuthority.decidePer ? yield* tx.get<{ state: string }>(sql` SELECT state FROM session_legacy_activity WHERE activity_id = ${request.activity_id} `) - : yield* tx - .select({ state: SessionActivityTable.state }) - .from(SessionActivityTable) - .where(eq(SessionActivityTable.activity_id, request.activity_id)) - .get() + : request.activity_kind === "facade" + ? yield* tx + .select({ state: SessionFacadeActivityTable.state }) + .from(SessionFacadeActivityTable) + .where(eq(SessionFacadeActivityTable.activity_id, request.activity_id)) + .get() + : yield* tx + .select({ state: SessionActivityTable.state }) + .from(SessionActivityTable) + .where(eq(SessionActivityTable.activity_id, request.activity_id)) + .get() if (objective?.state !== "active" || base?.state !== "active") return yield* new ConflictError({ entity: request.activity_id, @@ -981,17 +1074,30 @@ const decidePermissionInternal = Effect.fn("DeepAgentActivityAuthority.decidePer WHERE activity_id = ${request.activity_id} AND state = 'active' RETURNING activity_id `) - : yield* tx - .update(SessionActivityTable) - .set({ state: "interrupted", settled_at: now }) - .where( - and( - eq(SessionActivityTable.activity_id, request.activity_id), - eq(SessionActivityTable.state, "active"), - ), - ) - .returning({ activityID: SessionActivityTable.activity_id }) - .get() + : request.activity_kind === "facade" + ? // FEAT-011 T2 — facade interruption mirrors the v2 branch (CAS on active). + yield* tx + .update(SessionFacadeActivityTable) + .set({ state: "interrupted", reason_code: terminalReason, settled_at: now }) + .where( + and( + eq(SessionFacadeActivityTable.activity_id, request.activity_id), + eq(SessionFacadeActivityTable.state, "active"), + ), + ) + .returning({ activityID: SessionFacadeActivityTable.activity_id }) + .get() + : yield* tx + .update(SessionActivityTable) + .set({ state: "interrupted", settled_at: now }) + .where( + and( + eq(SessionActivityTable.activity_id, request.activity_id), + eq(SessionActivityTable.state, "active"), + ), + ) + .returning({ activityID: SessionActivityTable.activity_id }) + .get() if (!base) return yield* new ConflictError({ entity: request.activity_id, @@ -1326,11 +1432,17 @@ export const consumeOnce = Effect.fn("DeepAgentActivityAuthority.consumeOnce")(f ? yield* tx.get<{ state: string }>(sql` SELECT state FROM session_legacy_activity WHERE activity_id = ${request.activity_id} `) - : yield* tx - .select({ state: SessionActivityTable.state }) - .from(SessionActivityTable) - .where(eq(SessionActivityTable.activity_id, request.activity_id)) - .get() + : request.activity_kind === "facade" + ? yield* tx + .select({ state: SessionFacadeActivityTable.state }) + .from(SessionFacadeActivityTable) + .where(eq(SessionFacadeActivityTable.activity_id, request.activity_id)) + .get() + : yield* tx + .select({ state: SessionActivityTable.state }) + .from(SessionActivityTable) + .where(eq(SessionActivityTable.activity_id, request.activity_id)) + .get() if (objective?.state !== "active" || base?.state !== "active") return yield* new ConflictError({ entity: request.activity_id, @@ -1460,11 +1572,17 @@ export const beginPermissionEffect = Effect.fn("DeepAgentActivityAuthority.begin ? yield* tx.get<{ state: string }>(sql` SELECT state FROM session_legacy_activity WHERE activity_id = ${request.activity_id} `) - : yield* tx - .select({ state: SessionActivityTable.state }) - .from(SessionActivityTable) - .where(eq(SessionActivityTable.activity_id, request.activity_id)) - .get() + : request.activity_kind === "facade" + ? yield* tx + .select({ state: SessionFacadeActivityTable.state }) + .from(SessionFacadeActivityTable) + .where(eq(SessionFacadeActivityTable.activity_id, request.activity_id)) + .get() + : yield* tx + .select({ state: SessionActivityTable.state }) + .from(SessionActivityTable) + .where(eq(SessionActivityTable.activity_id, request.activity_id)) + .get() if (objective?.state !== "active" || base?.state !== "active") return yield* new ConflictError({ entity: request.activity_id, @@ -1952,17 +2070,30 @@ function recoverPendingPermissionsInTransaction( WHERE activity_id = ${request.activity_id} AND state = 'active' RETURNING activity_id `) - : yield* tx - .update(SessionActivityTable) - .set({ state: "interrupted", settled_at: now }) - .where( - and( - eq(SessionActivityTable.activity_id, request.activity_id), - eq(SessionActivityTable.state, "active"), - ), - ) - .returning({ activityID: SessionActivityTable.activity_id }) - .get() + : request.activity_kind === "facade" + ? // FEAT-011 T2 — facade interruption mirrors the v2 branch (CAS on active). + yield* tx + .update(SessionFacadeActivityTable) + .set({ state: "interrupted", reason_code: terminalReason, settled_at: now }) + .where( + and( + eq(SessionFacadeActivityTable.activity_id, request.activity_id), + eq(SessionFacadeActivityTable.state, "active"), + ), + ) + .returning({ activityID: SessionFacadeActivityTable.activity_id }) + .get() + : yield* tx + .update(SessionActivityTable) + .set({ state: "interrupted", settled_at: now }) + .where( + and( + eq(SessionActivityTable.activity_id, request.activity_id), + eq(SessionActivityTable.state, "active"), + ), + ) + .returning({ activityID: SessionActivityTable.activity_id }) + .get() if (!base) return yield* new ConflictError({ entity: request.activity_id, @@ -2191,17 +2322,31 @@ function recoverActivityToRequired( WHERE activity_id = ${objective.activity_id} AND state = 'active' RETURNING activity_id `) - : yield* tx - .update(SessionActivityTable) - .set({ state: "failed", settled_at: now }) - .where( - and( - eq(SessionActivityTable.activity_id, objective.activity_id), - eq(SessionActivityTable.state, "active"), - ), - ) - .returning({ activityID: SessionActivityTable.activity_id }) - .get() + : objective.activity_kind === "facade" + ? // FEAT-011 T2 — facade recovery follows the v2 mapping: recovery_required is persisted + // as base state 'failed' (CAS on active), unlike legacy which keeps 'recovery_required'. + yield* tx + .update(SessionFacadeActivityTable) + .set({ state: "failed", reason_code: terminalReason, settled_at: now }) + .where( + and( + eq(SessionFacadeActivityTable.activity_id, objective.activity_id), + eq(SessionFacadeActivityTable.state, "active"), + ), + ) + .returning({ activityID: SessionFacadeActivityTable.activity_id }) + .get() + : yield* tx + .update(SessionActivityTable) + .set({ state: "failed", settled_at: now }) + .where( + and( + eq(SessionActivityTable.activity_id, objective.activity_id), + eq(SessionActivityTable.state, "active"), + ), + ) + .returning({ activityID: SessionActivityTable.activity_id }) + .get() if (!base) return yield* new ConflictError({ entity: objective.activity_id, @@ -2266,11 +2411,17 @@ function reconcilePermissionFanout( ? yield* tx.get<{ state: string }>(sql` SELECT state FROM session_legacy_activity WHERE activity_id = ${sibling.activity_id} `) - : yield* tx - .select({ state: SessionActivityTable.state }) - .from(SessionActivityTable) - .where(eq(SessionActivityTable.activity_id, sibling.activity_id)) - .get() + : sibling.activity_kind === "facade" + ? yield* tx + .select({ state: SessionFacadeActivityTable.state }) + .from(SessionFacadeActivityTable) + .where(eq(SessionFacadeActivityTable.activity_id, sibling.activity_id)) + .get() + : yield* tx + .select({ state: SessionActivityTable.state }) + .from(SessionActivityTable) + .where(eq(SessionActivityTable.activity_id, sibling.activity_id)) + .get() const expired = sibling.expires_at !== null && sibling.expires_at <= input.decidedAt const activityTerminal = !objective || @@ -2372,17 +2523,30 @@ function reconcilePermissionFanout( WHERE activity_id = ${sibling.activity_id} AND state = 'active' RETURNING activity_id `) - : yield* tx - .update(SessionActivityTable) - .set({ state: "interrupted", settled_at: input.decidedAt }) - .where( - and( - eq(SessionActivityTable.activity_id, sibling.activity_id), - eq(SessionActivityTable.state, "active"), - ), - ) - .returning({ activityID: SessionActivityTable.activity_id }) - .get() + : sibling.activity_kind === "facade" + ? // FEAT-011 T2 — facade interruption mirrors the v2 branch (CAS on active). + yield* tx + .update(SessionFacadeActivityTable) + .set({ state: "interrupted", reason_code: terminalReason, settled_at: input.decidedAt }) + .where( + and( + eq(SessionFacadeActivityTable.activity_id, sibling.activity_id), + eq(SessionFacadeActivityTable.state, "active"), + ), + ) + .returning({ activityID: SessionFacadeActivityTable.activity_id }) + .get() + : yield* tx + .update(SessionActivityTable) + .set({ state: "interrupted", settled_at: input.decidedAt }) + .where( + and( + eq(SessionActivityTable.activity_id, sibling.activity_id), + eq(SessionActivityTable.state, "active"), + ), + ) + .returning({ activityID: SessionActivityTable.activity_id }) + .get() if (!interrupted) return yield* new ConflictError({ entity: sibling.activity_id, diff --git a/packages/core/src/deepagent/deepagent-event-bus.ts b/packages/core/src/deepagent/deepagent-event-bus.ts index 1791352b..5256bec6 100644 --- a/packages/core/src/deepagent/deepagent-event-bus.ts +++ b/packages/core/src/deepagent/deepagent-event-bus.ts @@ -4,7 +4,7 @@ import { Context, Effect, Layer, PubSub, Stream } from "effect" import { and, asc, desc, eq, isNull, lte, lt, gt, notExists, or, sql } from "drizzle-orm" import { alias } from "drizzle-orm/sqlite-core" import { Database } from "../database/database" -import { DeepAgentEventDeliveryTable, DeepAgentEventDropTable, DeepAgentEventTable, DeepAgentConsumerGroupTable } from "./deepagent-event-sql" +import { DeepAgentEventDeliveryTable, DeepAgentEventDropTable, DeepAgentEventTable, DeepAgentConsumerGroupTable, DeepAgentRateLimitBucketTable } from "./deepagent-event-sql" import { ApprovalQueueTable } from "./approval-queue-sql" import { DeepAgentEvent } from "./deepagent-event" import { RateLimiter } from "./rate-limiter" @@ -71,8 +71,10 @@ export interface Interface { * NOT persisted (no row, no dispatch). Otherwise the persisted event is returned as `{ published }`. * The discriminated union lets the caller observe the drop (e.g. to log a blocked-push counter). * - * Keyed per workspaceID (fixed-window, in-memory). This is ADDITIVE — existing `publish` callers are - * untouched and bypass the gate entirely. + * Keyed per workspaceID (fixed-window, DURABLE — the bucket lives in `deepagent_rate_limit_bucket` + * and is updated inside an immediate transaction, so N processes sharing the DB enforce ONE + * combined quota; RISK-006). This is ADDITIVE — existing `publish` callers are untouched and + * bypass the gate entirely. */ readonly tryPublish: ( input: DeepAgentEvent.PublishInput, @@ -134,6 +136,32 @@ export interface Interface { }) => Effect.Effect /** §A3 retry scan — pending deliveries whose backoff has elapsed (Router/Scheduler drives re-delivery). */ readonly dueRetries: (now?: number) => Effect.Effect> + /** + * RISK-001: atomically claim due pending deliveries for ONE subscription group. Stamps a fresh + * claim token + lease inside an immediate transaction; rows under a live lease are skipped and + * expired leases are re-claimable. The returned opaque token must be passed to ackClaim/nackClaim + * so a stale worker cannot settle a delivery that a newer claimant already owns. + */ + readonly claimDue: (input: { + readonly subscriptionGroup: string + readonly claimantId: string + readonly now?: number + readonly limit?: number + readonly leaseMs?: number + }) => Effect.Effect<{ readonly claimToken: string; readonly deliveries: ReadonlyArray }> + /** Ack conditioned on the claim token; a stale token (or non-pending row) is a no-op → false. */ + readonly ackClaim: (input: { + readonly subscriptionGroup: string + readonly eventID: DeepAgentEvent.ID + readonly claimToken: string + }) => Effect.Effect + /** Nack conditioned on the claim token; a stale token is a no-op → false. Clears the claim. */ + readonly nackClaim: (input: { + readonly subscriptionGroup: string + readonly eventID: DeepAgentEvent.ID + readonly claimToken: string + readonly reason: string + }) => Effect.Effect /** * K40-4: real pending-delivery count per workspace — the durable backlog depth for the * backpressure gate. Counts deepagent_event_delivery rows with status='pending' scoped @@ -163,11 +191,10 @@ export interface Interface { readonly limit?: number }) => Effect.Effect<{ readonly deletedEvents: number }> /** - * §E2 — prune the publish rate-limiter's per-workspace buckets whose fixed window has elapsed as of - * `now`, bounding the limiter's memory for idle workspaces. Returns how many buckets were dropped. - * The limiter lives inside this layer's closure; this exposes its `sweep` so a periodic daemon - * (v4-event-runtime) can drive it on a cadence without reaching into private state. `now` defaults to - * the injected clock (deterministic in tests). A no-op that never throws — safe to call any time. + * §E2 — prune the publish rate-limiter's durable per-workspace buckets + * (`deepagent_rate_limit_bucket`) whose fixed window has elapsed as of `now`, bounding the table + * for idle workspaces. Returns how many buckets were dropped. `now` defaults to the injected + * clock (deterministic in tests). A no-op that never throws — safe to call any time. */ readonly sweepPublishLimiter: (now?: number) => Effect.Effect<{ readonly prunedBuckets: number }> /** @@ -176,8 +203,11 @@ export interface Interface { * With durable registration, `publish` writes delivery rows for ALL registered groups — not just the * in-memory live ones — so an offline consumer catches up via `dueRetries` + `replay` on reconnect. * Idempotent: re-registering the same group updates `last_seen_at`. + * + * RISK-006: `workspaceID` records the tenant the group consumes for; defaults to "" (global / + * unscoped) so existing callers stay source-compatible. */ - readonly registerConsumerGroup: (groupId: string, typeFilter?: string) => Effect.Effect + readonly registerConsumerGroup: (groupId: string, typeFilter?: string, workspaceID?: string) => Effect.Effect /** * K40-2: Durably unregister a consumer group. After this call `publish` will no longer create delivery * rows for this group. Live in-memory streams for this group are NOT terminated — they finish naturally. @@ -187,6 +217,16 @@ export interface Interface { export class Service extends Context.Service()("@deepagent-code/DeepAgentEventBus") {} +// RISK-006 — durable priority ordering for dueRetries/claimDue: critical(3) > high(2) > normal(1) +// > low(0). NULL (pre-migration rows) falls into ELSE and ranks as "normal". Combined with +// next_attempt_at ASC this keeps FIFO order WITHIN a priority class. +const priorityRank = sql`case + when ${DeepAgentEventDeliveryTable.priority} = 'critical' then 3 + when ${DeepAgentEventDeliveryTable.priority} = 'high' then 2 + when ${DeepAgentEventDeliveryTable.priority} = 'low' then 0 + else 1 +end` + export interface LayerOptions { readonly maxAttempts?: number readonly backoffBaseMs?: number @@ -236,10 +276,6 @@ export const layerWith = (options?: LayerOptions) => // `subscribe` for grouped consumers merges both channels (highPriorityLive first), so when // multiple events are queued, critical/high are processed before normal/low. const highPriorityLive = yield* PubSub.unbounded() - // §A4/§E2 — ONE in-memory fixed-window limiter for the whole bus, keyed per workspaceID. Only - // `tryPublish` consults it; `publish` is unchanged. `now` (the injected clock) drives window - // resets so tests cross a boundary deterministically. - const publishLimiter = new RateLimiter.Service() yield* Effect.addFinalizer(() => Effect.all([PubSub.shutdown(live), PubSub.shutdown(highPriorityLive)], { concurrency: "unbounded" }).pipe( @@ -285,8 +321,9 @@ export const layerWith = (options?: LayerOptions) => const dbGroupsCache = new Map() const invalidateDbGroupsCache = () => dbGroupsCache.clear() - const groupsFor = (eventType: string): Effect.Effect> => { - const cached = dbGroupsCache.get(eventType) + const groupsFor = (eventType: string, workspaceID: string): Effect.Effect> => { + const cacheKey = `${eventType}\n${workspaceID}` + const cached = dbGroupsCache.get(cacheKey) if (cached !== undefined) { // DB portion is cached; still merge with current live groups (always fresh, no DB). const live = liveGroupsFor(eventType) @@ -300,14 +337,24 @@ export const layerWith = (options?: LayerOptions) => .select({ group_id: DeepAgentConsumerGroupTable.group_id }) .from(DeepAgentConsumerGroupTable) .where( - or(isNull(DeepAgentConsumerGroupTable.type_filter), eq(DeepAgentConsumerGroupTable.type_filter, eventType)), + and( + or(isNull(DeepAgentConsumerGroupTable.type_filter), eq(DeepAgentConsumerGroupTable.type_filter, eventType)), + // RISK-006 residual: dispatch routing is workspace-scoped. A workspace-bound consumer + // group (workspace_id != "") is owed only its own tenant's events; a global group + // (workspace_id = "") keeps receiving every workspace. Live in-memory groups carry no + // workspace, so they remain global (all current consumers register globally). + or( + eq(DeepAgentConsumerGroupTable.workspace_id, ""), + eq(DeepAgentConsumerGroupTable.workspace_id, workspaceID), + ), + ), ) .all() .pipe( Effect.orDie, Effect.map((rows) => { const dbGroups = rows.map((r) => r.group_id) - dbGroupsCache.set(eventType, dbGroups) + dbGroupsCache.set(cacheKey, dbGroups) const live = liveGroupsFor(eventType) // Union: db-registered + live-only (not yet durable-registered), deduplicated. const seen = new Set(dbGroups) @@ -352,7 +399,7 @@ export const layerWith = (options?: LayerOptions) => // past the read-check above hits UNIQUE(idempotency_key) → 0 rows → not the winner). Dispatch // happens AFTER commit, so a subscriber never observes an uncommitted event. // K40-2: groupsFor is now async (queries DB for durable groups + in-memory live groups). - const owed = yield* groupsFor(event.type) + const owed = yield* groupsFor(event.type, event.workspaceID) // §F1 event_publish_latency_ms — wall-clock delta (injected clock) around the persist // transaction. One now() before, one after; the delta is written on the row so Observability // can build the publish-latency histogram. Cheap + additive. @@ -395,6 +442,9 @@ export const layerWith = (options?: LayerOptions) => attempts: 0, last_error: null, next_attempt_at: createdAt, // owed immediately until acked + // RISK-006: denormalize the event's priority onto the delivery row so the + // retry/claim scans can order high-priority work first without a join. + priority: event.priority, created_at: createdAt, updated_at: createdAt, })), @@ -455,16 +505,68 @@ export const layerWith = (options?: LayerOptions) => return { published: yield* publish(input) } } const limit = opts?.limit ?? RateLimiter.EVENT_PUBLISH_PER_WORKSPACE.limit - const admitted = publishLimiter.check( - input.workspaceID, - limit, - RateLimiter.EVENT_PUBLISH_PER_WORKSPACE.windowMs, - now(), - ) + const admitted = yield* checkDurableLimit(input.workspaceID, limit, now()) if (!admitted) return { dropped: "rate_limited" as const } return { published: yield* publish(input) } }) + // RISK-006 §A4/§E2 — DURABLE fixed-window check-and-increment against + // `deepagent_rate_limit_bucket`. The immediate transaction takes the SQLite write lock up front, + // so the read-modify-write below cannot interleave with another process/bus instance: every + // producer sharing the DB consumes ONE combined per-workspace quota (the old in-memory Map gave + // each process its own). Windows are epoch-aligned (floor(now / windowMs)) so independent + // processes agree on the current window without coordination. `now` (the injected clock) drives + // window boundaries so tests cross them deterministically. + const checkDurableLimit = (workspaceID: string, limit: number, at: number) => + db + .transaction( + () => + Effect.gen(function* () { + const windowMs = RateLimiter.EVENT_PUBLISH_PER_WORKSPACE.windowMs + const windowStart = Math.floor(at / windowMs) * windowMs + const bucket = yield* db + .select() + .from(DeepAgentRateLimitBucketTable) + .where( + and( + eq(DeepAgentRateLimitBucketTable.workspace_id, workspaceID), + eq(DeepAgentRateLimitBucketTable.window_start, windowStart), + ), + ) + .get() + .pipe(Effect.orDie) + if (bucket) { + if (bucket.count >= limit) return false + yield* db + .update(DeepAgentRateLimitBucketTable) + .set({ count: bucket.count + 1 }) + .where( + and( + eq(DeepAgentRateLimitBucketTable.workspace_id, workspaceID), + eq(DeepAgentRateLimitBucketTable.window_start, windowStart), + ), + ) + .run() + .pipe(Effect.orDie) + return true + } + // Fresh window — seed count=1. A conflict cannot happen while we hold the write lock, + // but the upsert keeps the insert total should that ever change. + yield* db + .insert(DeepAgentRateLimitBucketTable) + .values([{ workspace_id: workspaceID, window_start: windowStart, count: 1 }]) + .onConflictDoUpdate({ + target: [DeepAgentRateLimitBucketTable.workspace_id, DeepAgentRateLimitBucketTable.window_start], + set: { count: 1 }, + }) + .run() + .pipe(Effect.orDie) + return true + }), + { behavior: "immediate" }, + ) + .pipe(Effect.orDie) + const subscribe: Interface["subscribe"] = (input) => { const filtered = Stream.fromPubSub(live).pipe( Stream.filter((event) => (input.type ? event.type === input.type : true)), @@ -747,12 +849,158 @@ export const layerWith = (options?: LayerOptions) => lte(DeepAgentEventDeliveryTable.next_attempt_at, at), ), ) - .orderBy(asc(DeepAgentEventDeliveryTable.next_attempt_at)) + // RISK-006: persistent priority — high/critical due work drains before normal/low. + .orderBy(desc(priorityRank), asc(DeepAgentEventDeliveryTable.next_attempt_at)) .all() .pipe(Effect.orDie) return rows.map(trackerOf) }) + // RISK-001: locked replacement for the dueRetries scan. The immediate transaction takes the + // SQLite write lock up front, so the SELECT-then-UPDATE below cannot interleave with another + // writer — exactly one claimant wins each row while its lease is live. + const claimDue: Interface["claimDue"] = (input) => + Effect.uninterruptible( + db + .transaction( + () => + Effect.gen(function* () { + const at = input.now ?? now() + const leaseMs = input.leaseMs ?? 5 * 60_000 + const limit = input.limit ?? 50 + const eligible = yield* db + .select() + .from(DeepAgentEventDeliveryTable) + .where( + and( + eq(DeepAgentEventDeliveryTable.subscription_group, input.subscriptionGroup), + eq(DeepAgentEventDeliveryTable.status, "pending"), + lte(DeepAgentEventDeliveryTable.next_attempt_at, at), + or( + isNull(DeepAgentEventDeliveryTable.claim_token), + lte(DeepAgentEventDeliveryTable.lease_expires_at, at), + ), + ), + ) + // RISK-006: persistent priority — a limited claim takes high/critical rows first. + .orderBy(desc(priorityRank), asc(DeepAgentEventDeliveryTable.next_attempt_at)) + .limit(limit) + .all() + .pipe(Effect.orDie) + const claimToken = crypto.randomUUID() + if (eligible.length === 0) return { claimToken, deliveries: [] as ReadonlyArray } + yield* db + .update(DeepAgentEventDeliveryTable) + .set({ + claim_token: claimToken, + claimant_id: input.claimantId, + claimed_at: at, + lease_expires_at: at + leaseMs, + updated_at: at, + }) + .where( + or( + ...eligible.map((row) => + and( + eq(DeepAgentEventDeliveryTable.event_id, row.event_id), + eq(DeepAgentEventDeliveryTable.subscription_group, row.subscription_group), + ), + ), + ), + ) + .run() + .pipe(Effect.orDie) + return { claimToken, deliveries: eligible.map(trackerOf) } + }), + { behavior: "immediate" }, + ) + .pipe(Effect.orDie), + ) + + const ackClaim: Interface["ackClaim"] = (input) => + db + .update(DeepAgentEventDeliveryTable) + .set({ + status: "delivered", + last_error: null, + next_attempt_at: null, + updated_at: now(), + }) + .where( + and( + eq(DeepAgentEventDeliveryTable.event_id, input.eventID), + eq(DeepAgentEventDeliveryTable.subscription_group, input.subscriptionGroup), + eq(DeepAgentEventDeliveryTable.claim_token, input.claimToken), + eq(DeepAgentEventDeliveryTable.status, "pending"), + ), + ) + .returning({ eventID: DeepAgentEventDeliveryTable.event_id }) + .all() + .pipe(Effect.map((rows) => rows.length > 0), Effect.orDie) + + const nackClaim: Interface["nackClaim"] = (input) => + Effect.gen(function* () { + const transition = yield* Effect.uninterruptible( + db + .transaction( + () => + Effect.gen(function* () { + const current = yield* db + .select() + .from(DeepAgentEventDeliveryTable) + .where( + and( + eq(DeepAgentEventDeliveryTable.event_id, input.eventID), + eq(DeepAgentEventDeliveryTable.subscription_group, input.subscriptionGroup), + eq(DeepAgentEventDeliveryTable.claim_token, input.claimToken), + ), + ) + .get() + .pipe(Effect.orDie) + // Stale token or a row that already settled — the claim no longer owns it. + if (!current || current.status !== "pending") + return { applied: false, justDied: false, attempts: 0 } + const attempts = current.attempts + 1 + const dead = attempts >= maxAttempts + const at = now() + const nextAttemptAt = dead ? null : at + backoffBaseMs * 2 ** (attempts - 1) + yield* db + .update(DeepAgentEventDeliveryTable) + .set({ + status: dead ? "dead" : "pending", + attempts, + last_error: input.reason, + next_attempt_at: nextAttemptAt, + // release the claim: a pending row is eligible for the next claimant; a dead + // row never re-enters the retry scan. + claim_token: null, + claimant_id: null, + claimed_at: null, + lease_expires_at: null, + updated_at: at, + }) + .where( + and( + eq(DeepAgentEventDeliveryTable.event_id, input.eventID), + eq(DeepAgentEventDeliveryTable.subscription_group, input.subscriptionGroup), + eq(DeepAgentEventDeliveryTable.claim_token, input.claimToken), + ), + ) + .run() + .pipe(Effect.orDie) + return { applied: true, justDied: dead, attempts } + }), + { behavior: "immediate" }, + ) + .pipe(Effect.orDie), + ) + if (transition.justDied) + yield* emitDlqAlert(input.eventID, input.subscriptionGroup, input.reason, transition.attempts).pipe( + Effect.catchCause(() => Effect.void), + ) + return transition.applied + }) + // K40-4: real durable backlog depth per workspace — count pending delivery rows whose source event // belongs to the given workspace. This is the authoritative backpressure signal: it reflects the // number of events that are owed to at least one consumer group but not yet acked. Uses the existing @@ -873,22 +1121,41 @@ export const layerWith = (options?: LayerOptions) => ) .pipe(Effect.orDie) - // §E2 — drive the in-memory rate-limiter's stale-window prune. Synchronous + total (never fails), - // wrapped in Effect.sync so the daemon can schedule it uniformly with the other bus effects. + // §E2 — prune elapsed durable rate-limit windows (RISK-006). A bucket's window has elapsed once + // `window_start + windowMs <= now`. Returns how many buckets were dropped. const sweepPublishLimiter: Interface["sweepPublishLimiter"] = (nowArg) => - Effect.sync(() => ({ prunedBuckets: publishLimiter.sweep(nowArg ?? now()) })) + Effect.gen(function* () { + const at = nowArg ?? now() + const windowMs = RateLimiter.EVENT_PUBLISH_PER_WORKSPACE.windowMs + const pruned = yield* db + .delete(DeepAgentRateLimitBucketTable) + .where(lte(DeepAgentRateLimitBucketTable.window_start, at - windowMs)) + .returning({ workspace_id: DeepAgentRateLimitBucketTable.workspace_id }) + .all() + .pipe(Effect.orDie) + return { prunedBuckets: pruned.length } + }) // K40-2: durable consumer group registration — persists group identity so publish writes delivery // rows for offline groups too. Both methods are idempotent; registerConsumerGroup upserts. // PERF: both methods invalidate dbGroupsCache so the next groupsFor re-reads from DB. - const registerConsumerGroup: Interface["registerConsumerGroup"] = (groupId, typeFilter) => { + const registerConsumerGroup: Interface["registerConsumerGroup"] = (groupId, typeFilter, workspaceID) => { const at = now() + const workspace = workspaceID ?? "" return db .insert(DeepAgentConsumerGroupTable) - .values([{ group_id: groupId, type_filter: typeFilter ?? null, registered_at: at, last_seen_at: at }]) + .values([ + { + group_id: groupId, + type_filter: typeFilter ?? null, + workspace_id: workspace, + registered_at: at, + last_seen_at: at, + }, + ]) .onConflictDoUpdate({ target: DeepAgentConsumerGroupTable.group_id, - set: { type_filter: typeFilter ?? null, last_seen_at: at }, + set: { type_filter: typeFilter ?? null, workspace_id: workspace, last_seen_at: at }, }) .run() .pipe(Effect.orDie, Effect.asVoid, Effect.tap(() => Effect.sync(invalidateDbGroupsCache))) @@ -907,6 +1174,9 @@ export const layerWith = (options?: LayerOptions) => subscribe, ack, nack, + claimDue, + ackClaim, + nackClaim, replay, recentByType, deadLetters, diff --git a/packages/core/src/deepagent/deepagent-event-sql.ts b/packages/core/src/deepagent/deepagent-event-sql.ts index 2ff69ec4..0f05e639 100644 --- a/packages/core/src/deepagent/deepagent-event-sql.ts +++ b/packages/core/src/deepagent/deepagent-event-sql.ts @@ -1,4 +1,4 @@ -import { sqliteTable, text, integer, index, uniqueIndex } from "drizzle-orm/sqlite-core" +import { index, integer, primaryKey, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core" import type { DeepAgentEvent } from "./deepagent-event" // V4.0 §A3 — durable persistence for the DeepAgent Event Bus. The design principle is "事件先持久化, @@ -57,6 +57,16 @@ export const DeepAgentEventDeliveryTable = sqliteTable( attempts: integer().notNull(), last_error: text(), next_attempt_at: integer(), + // RISK-001: retry claim/lease. A pump atomically stamps token+claimant+lease before executing a + // due row; expired leases re-open the row for another claimant (migration 20260815120000). + claim_token: text(), + claimant_id: text(), + claimed_at: integer(), + lease_expires_at: integer(), + // RISK-006: denormalized event priority (written by publish) so dueRetries/claimDue can order + // high-priority work first without joining the event table. NULL = pre-migration rows, treated + // as "normal" by the ordering (migration 20260815130000). + priority: text().$type(), created_at: integer().notNull(), updated_at: integer().notNull(), }, @@ -65,6 +75,8 @@ export const DeepAgentEventDeliveryTable = sqliteTable( uniqueIndex("deepagent_event_delivery_unique_idx").on(table.event_id, table.subscription_group), // retry scan: pending rows whose backoff has elapsed, oldest first. index("deepagent_event_delivery_due_idx").on(table.status, table.next_attempt_at), + // group-scoped claim scan (RISK-001). + index("deepagent_event_delivery_claim_idx").on(table.subscription_group, table.status, table.next_attempt_at), ], ) @@ -79,6 +91,9 @@ export const DeepAgentConsumerGroupTable = sqliteTable( group_id: text().primaryKey(), // null = wildcard (all event types); non-null = subscribe to one type only. type_filter: text(), + // RISK-006: the tenant this group consumes for. Default "" keeps pre-existing global/unscoped + // registrations valid (migration 20260815130000). + workspace_id: text().notNull().default(""), registered_at: integer().notNull(), // Updated on every live subscribe/unsubscribe so a sweep can distinguish stale registrations. last_seen_at: integer().notNull(), @@ -86,6 +101,26 @@ export const DeepAgentConsumerGroupTable = sqliteTable( (table) => [ // fast lookup: which groups are registered for a given event type? index("deepagent_consumer_group_type_idx").on(table.type_filter), + // RISK-006: tenant-scoped group scans. + index("deepagent_consumer_group_workspace_idx").on(table.workspace_id), + ], +) + +// RISK-006 — durable fixed-window rate-limit buckets for the bus's tryPublish gate. One row per +// (workspace, window); `window_start` is the epoch-aligned window start (floor(now / windowMs)). +// The check-and-increment runs inside an immediate transaction, so N processes sharing the DB +// enforce ONE combined quota per workspace (the in-memory Map gave each process its own ×N). +export const DeepAgentRateLimitBucketTable = sqliteTable( + "deepagent_rate_limit_bucket", + { + workspace_id: text().notNull(), + window_start: integer().notNull(), + count: integer().notNull(), + }, + (table) => [ + primaryKey({ columns: [table.workspace_id, table.window_start] }), + // sweepPublishLimiter prunes elapsed windows by window_start alone (cross-workspace). + index("deepagent_rate_limit_bucket_window_idx").on(table.window_start), ], ) // §A4 event_dropped — the durable DROP LOG. One append-only row per event the router shed (a §A4 diff --git a/packages/core/src/deepagent/domain-pack.ts b/packages/core/src/deepagent/domain-pack.ts index a61d64be..a3e8265a 100644 --- a/packages/core/src/deepagent/domain-pack.ts +++ b/packages/core/src/deepagent/domain-pack.ts @@ -13,6 +13,14 @@ export type ProblemProfile = { } export type ActivateOptions = { + // FEAT-001: `override` is the legacy single-pack pin; `overrides` carries multi-pin (GUI pinned + // packs). Both are honored — single-value callers keep working unchanged. readonly override?: string + readonly overrides?: readonly string[] readonly threshold?: number } + +// Merge single + multi pin forms into one deduplicated list (FEAT-001 override type evolution). +export const overridePackIds = (options?: ActivateOptions): readonly string[] => [ + ...new Set([...(options?.override ? [options.override] : []), ...(options?.overrides ?? [])]), +] diff --git a/packages/core/src/deepagent/handoff-admission-sql.ts b/packages/core/src/deepagent/handoff-admission-sql.ts new file mode 100644 index 00000000..ceb61b65 --- /dev/null +++ b/packages/core/src/deepagent/handoff-admission-sql.ts @@ -0,0 +1,49 @@ +import { index, sqliteTable, text, integer, uniqueIndex } from "drizzle-orm/sqlite-core" +import type { DeepAgentEvent } from "./deepagent-event" + +// FEAT-008 — durable admission receipt for `agent.handoff.requested` processing. +// +// WHY A NEW TABLE (and not reuse of an existing one): +// - `deepagent_event_delivery` tracks DELIVERY lifecycle only (pending → delivered | dead). It has +// no handoff identity and cannot express the BUSINESS terminal states accepted/rejected, so after +// a crash it cannot answer "未处理 / 处理中 / 已完成" for a handoff request — only "was the event +// acked by this group". +// - `task_admission` is the subagent control-plane receipt for task_run admission (keyed by +// run/origin_key); its schema and idempotency semantics are orthogonal to handoff admission. +// - `agent_execution` is the per-task execution state machine; it is only touched once the handoff +// passes validation (a reject during validation — missing original event, security gate, etc. — +// must still leave a durable "rejected" receipt), and it never models the in-flight "processing" +// admission of the consumer itself. +// +// One row per handoffID. The consumer writes `processing` at the START of handling (before any +// side-effecting decision) and settles to `accepted`/`rejected` on the terminal outcome. A row stuck +// in `processing` after a crash means "not finished" — the retry pump re-admits it (begin() re-stamps +// the claimant) once the delivery claim's lease lapses; a TERMINAL row is never overwritten, so a +// redelivery short-circuits without re-running side effects. +export const DeepAgentHandoffAdmissionTable = sqliteTable( + "deepagent_handoff_admission", + { + // the handoff request's identity (payload.handoffID) — one admission per handoff request. + handoff_id: text().primaryKey(), + // the bus event that carries the handoff request (§F2 trace spine). + event_id: text().$type().notNull(), + workspace_id: text().notNull(), + // processing → accepted | rejected. `processing` rows are "not finished" and re-admissible. + state: text().$type<"processing" | "accepted" | "rejected">().notNull(), + // who is currently handling (or last settled) the admission — the delivery claim's claimant id + // on the pump path, a stable consumer tag on the live-subscription path. + claimant_id: text().notNull(), + // the terminal reject reason (mirrors the rejectHandoff reason), null while processing/accepted. + reason: text(), + started_at: integer().notNull(), + updated_at: integer().notNull(), + // when the admission reached its terminal state (null while processing). + settled_at: integer(), + }, + (table) => [ + // crash-recovery scan: outstanding (non-terminal) admissions per tenant. + index("deepagent_handoff_admission_workspace_state_idx").on(table.workspace_id, table.state), + // trace lookup: which admission a handoff event produced. + uniqueIndex("deepagent_handoff_admission_event_idx").on(table.event_id), + ], +) diff --git a/packages/core/src/deepagent/handoff-admission.ts b/packages/core/src/deepagent/handoff-admission.ts new file mode 100644 index 00000000..2e84f4b0 --- /dev/null +++ b/packages/core/src/deepagent/handoff-admission.ts @@ -0,0 +1,182 @@ +export * as HandoffAdmission from "./handoff-admission" + +import { Context, Effect, Layer } from "effect" +import { and, eq } from "drizzle-orm" +import { Database } from "../database/database" +import type { DeepAgentEvent } from "./deepagent-event" +import { DeepAgentHandoffAdmissionTable } from "./handoff-admission-sql" + +// FEAT-008 — durable admission receipts for `agent.handoff.requested` handling (table: +// handoff-admission-sql.ts). Closes the "事件即状态" gap: before this receipt existed, a crash +// between receiving a handoff event and settling it was indistinguishable from "never processed" — +// the only record was the delivery row's ack/nack. Now every admission is durably stamped +// `processing` BEFORE any side-effecting decision and settled to a terminal state exactly once: +// +// no row → 未处理 (never admitted) +// processing → 处理中 — and after a crash this means "NOT finished": begin() re-admits it +// (re-stamps the claimant) instead of treating it as settled. Recovery itself is +// driven by the bus delivery claim/lease — an unacked delivery whose lease lapsed +// is re-claimed by the retry pump, which re-runs the consumer and thus begin(). +// accepted / → 已完成: TERMINAL rows are never overwritten, so a redelivery short-circuits +// rejected (begin() returns the terminal receipt) without re-running side effects. +export type State = "processing" | "accepted" | "rejected" + +export interface Receipt { + readonly handoffID: string + readonly eventID: DeepAgentEvent.ID + readonly workspaceID: string + readonly state: State + readonly claimantID: string + readonly reason?: string + readonly startedAt: number + readonly updatedAt: number + readonly settledAt?: number +} + +export interface Interface { + /** + * Admit a handoff request for processing. Inserts the `processing` receipt on first admission; a + * re-admission (crash recovery / redelivery) re-stamps claimant+updated_at ONLY while the row is + * still `processing`. A TERMINAL receipt is returned untouched — the caller must short-circuit. + */ + readonly begin: (input: { + readonly handoffID: string + readonly eventID: DeepAgentEvent.ID + readonly workspaceID: string + readonly claimantID: string + readonly at?: number + }) => Effect.Effect + /** + * Settle the admission to its terminal state. Conditioned on `state = 'processing'` — settling an + * already-terminal (or missing) receipt is a no-op → false, so a stale claimant can never flip a + * terminal receipt its successor already settled. + */ + readonly settle: (input: { + readonly handoffID: string + readonly state: "accepted" | "rejected" + readonly reason?: string + readonly at?: number + }) => Effect.Effect + readonly get: (handoffID: string) => Effect.Effect +} + +export class Service extends Context.Service()("@deepagent-code/HandoffAdmission") {} + +export interface LayerOptions { + readonly now?: () => number +} + +const decode = (row: typeof DeepAgentHandoffAdmissionTable.$inferSelect): Receipt => ({ + handoffID: row.handoff_id, + eventID: row.event_id, + workspaceID: row.workspace_id, + state: row.state, + claimantID: row.claimant_id, + ...(row.reason != null ? { reason: row.reason } : {}), + startedAt: row.started_at, + updatedAt: row.updated_at, + ...(row.settled_at != null ? { settledAt: row.settled_at } : {}), +}) + +export const layerWith = (options?: LayerOptions) => + Layer.effect( + Service, + Effect.gen(function* () { + const { db } = yield* Database.Service + const now = options?.now ?? Date.now + + const get: Interface["get"] = (handoffID) => + db + .select() + .from(DeepAgentHandoffAdmissionTable) + .where(eq(DeepAgentHandoffAdmissionTable.handoff_id, handoffID)) + .get() + .pipe( + Effect.orDie, + Effect.map((row) => (row ? decode(row) : undefined)), + ) + + const begin: Interface["begin"] = (input) => + Effect.gen(function* () { + const at = input.at ?? now() + const existing = yield* get(input.handoffID) + // 已完成 is sticky: a terminal receipt survives every re-admission (crash recovery, lease + // re-claim, duplicate redelivery) — the caller observes the terminal state and acks without + // re-running accept/reject side effects. + if (existing && existing.state !== "processing") return existing + if (existing) { + // 处理中 after a crash: still "not finished" — take it over (fresh claimant stamp). The + // `state = 'processing'` guard keeps a racing terminal settle from being overwritten. + yield* db + .update(DeepAgentHandoffAdmissionTable) + .set({ claimant_id: input.claimantID, updated_at: at }) + .where( + and( + eq(DeepAgentHandoffAdmissionTable.handoff_id, input.handoffID), + eq(DeepAgentHandoffAdmissionTable.state, "processing"), + ), + ) + .run() + .pipe(Effect.orDie) + return { ...existing, claimantID: input.claimantID, updatedAt: at } + } + // 未处理 → first admission. A racing writer that already settled the row wins the conflict; + // re-read so the caller always sees the authoritative (possibly terminal) receipt. + yield* db + .insert(DeepAgentHandoffAdmissionTable) + .values({ + handoff_id: input.handoffID, + event_id: input.eventID, + workspace_id: input.workspaceID, + state: "processing", + claimant_id: input.claimantID, + reason: null, + started_at: at, + updated_at: at, + settled_at: null, + }) + .onConflictDoNothing({ target: DeepAgentHandoffAdmissionTable.handoff_id }) + .run() + .pipe(Effect.orDie) + const settled = yield* get(input.handoffID) + return ( + settled ?? { + handoffID: input.handoffID, + eventID: input.eventID, + workspaceID: input.workspaceID, + state: "processing" as const, + claimantID: input.claimantID, + startedAt: at, + updatedAt: at, + } + ) + }) + + const settle: Interface["settle"] = (input) => + db + .update(DeepAgentHandoffAdmissionTable) + .set({ + state: input.state, + reason: input.reason ?? null, + updated_at: input.at ?? now(), + settled_at: input.at ?? now(), + }) + .where( + and( + eq(DeepAgentHandoffAdmissionTable.handoff_id, input.handoffID), + eq(DeepAgentHandoffAdmissionTable.state, "processing"), + ), + ) + .returning({ handoff_id: DeepAgentHandoffAdmissionTable.handoff_id }) + .all() + .pipe( + Effect.orDie, + Effect.map((rows) => rows.length > 0), + ) + + return Service.of({ begin, settle, get }) + }), + ) + +export const layer = layerWith() +export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer)) diff --git a/packages/core/src/deepagent/knowledge-retriever.ts b/packages/core/src/deepagent/knowledge-retriever.ts index baa135e5..aca8db1b 100644 --- a/packages/core/src/deepagent/knowledge-retriever.ts +++ b/packages/core/src/deepagent/knowledge-retriever.ts @@ -2,7 +2,7 @@ import type { AgentMode } from "./mode" import { knowledgeEnabled, strategyMethodologyEnabled, domainKnowledgeEnabled } from "./mode" import type { KnowledgeRefProjection, KnowledgeSynthesis, TaskContext, ToolContext } from "./prompt-policy" import * as knowledgeSource from "./knowledge-source" -import { type ProblemProfile, type ActivateOptions } from "./domain-pack" +import { type ProblemProfile, type ActivateOptions, overridePackIds } from "./domain-pack" import * as Registry from "./domain-pack-registry" import type { ExtendedProblemProfile } from "./domain-pack-registry" import { DeepAgentReleasedSnapshot, SnapshotIntegrityError } from "./released-snapshot" @@ -39,6 +39,10 @@ export type RetrievalInput = { readonly blockedRefs?: readonly string[] readonly profile?: ProblemProfile // V3: activates domain packs (docs/31 §2) readonly domainOptions?: ActivateOptions // override / threshold for domain activation + // FEAT-002: the authoritative run profile, built ONCE by the gateway (profile-builder). When + // present, pack activation uses it verbatim so the recorded active_pack_set and the retrieval + // constraints share one source of truth. Absent => profileFromInput() fallback (legacy path). + readonly profileOverride?: ExtendedProblemProfile // V3.2.1 decision B (docs/34 §8): workspace isolation by path. When set, durable retrieval unions // user-global knowledge with project-shared knowledge for THIS workspace; project-shared knowledge // from OTHER workspaces is never read (different on-disk store). Absent => user-global only. The @@ -1026,13 +1030,21 @@ const profileFromInput = (input: RetrievalInput): ExtendedProblemProfile => { ], repo_signals: [taskText(input), ...(input.profile?.signals ?? [])], round_signals: [...new Set(input.previousFailures > 0 ? ["previous_round_failure"] : [])], - user_overrides: input.domainOptions?.override ? [input.domainOptions.override] : [], + // FEAT-001: merge legacy single `override` with multi-pin `overrides` (GUI pinned packs). + user_overrides: [...overridePackIds(input.domainOptions)], } } -const activateKnowledgePacks = (input: RetrievalInput): KnowledgeActivation => { - const profile = profileFromInput(input) - const threshold = input.domainOptions?.threshold ?? 0.5 +// FEAT-002: the pack activation computed from ONE profile — score → threshold → user overrides → +// dependency/conflict resolution → core fallback packs. Exported so the gateway records the run's +// active_pack_set from the exact same activation its retrieval constrains on (single authority). +export type PackActivation = { + readonly activePackIds: readonly string[] + readonly primaryPackIds: readonly string[] + readonly activeDomains: readonly string[] +} + +export const activationForProfile = (profile: ExtendedProblemProfile, threshold = 0.5): PackActivation => { const manifests = Registry.discover() const selected = Registry.score(profile, manifests) .filter((s) => s.score >= threshold) @@ -1049,6 +1061,24 @@ const activateKnowledgePacks = (input: RetrievalInput): KnowledgeActivation => { .filter((id) => !CORE_FALLBACK_PACKS.includes(id as (typeof CORE_FALLBACK_PACKS)[number])) .sort(), activeDomains: [...new Set(activeManifests.flatMap((m) => m.domains))].sort(), + } +} + +const activateKnowledgePacks = (input: RetrievalInput): KnowledgeActivation => { + // FEAT-002: prefer the gateway-supplied authoritative profile; profileFromInput is the fallback + // when no profileOverride is handed in (legacy/unpinned callers keep their previous behavior). + const profile = input.profileOverride + ? { + ...input.profileOverride, + // Keep honoring domainOptions pins even when a profileOverride is supplied (FEAT-001). + user_overrides: [ + ...new Set([...input.profileOverride.user_overrides, ...overridePackIds(input.domainOptions)]), + ], + } + : profileFromInput(input) + const threshold = input.domainOptions?.threshold ?? 0.5 + return { + ...activationForProfile(profile, threshold), keywords: keywordsForTask(input), } } diff --git a/packages/core/src/deepagent/learning-lifecycle-trigger.ts b/packages/core/src/deepagent/learning-lifecycle-trigger.ts index 445379b7..59a2d7ec 100644 --- a/packages/core/src/deepagent/learning-lifecycle-trigger.ts +++ b/packages/core/src/deepagent/learning-lifecycle-trigger.ts @@ -31,8 +31,11 @@ export type ProjectBoundary = { export type ObserveInput = SessionBoundary | ProjectBoundary +// FEAT-004: the skipped reason is DISCRIMINATED — `no_observer_registered` means the notify seam had no +// runtime observer wired (pure CLI path / observer torn down), while `no_exact_settled_run` means an +// observer DID run but found no matching settled source run. Telemetry/operators can tell the two apart. export type Outcome = - | { readonly state: "skipped"; readonly reason: "no_exact_settled_run" } + | { readonly state: "skipped"; readonly reason: "no_exact_settled_run" | "no_observer_registered" } | { readonly state: "prepared" | "admitted"; readonly receiptId: string; readonly runId: string } export type RuntimeObserver = { @@ -65,8 +68,10 @@ export const setRuntimeObserver = (observer: RuntimeObserver | undefined) => { runtimeObserver = observer } +// FEAT-004: an UNREGISTERED observer is its own skip reason — the trigger never searched for a source +// run at all (distinct from observe() finding no matching settled run). export const notify = (input: ObserveInput): Promise => - runtimeObserver?.observe(input) ?? Promise.resolve({ state: "skipped", reason: "no_exact_settled_run" }) + runtimeObserver?.observe(input) ?? Promise.resolve({ state: "skipped", reason: "no_observer_registered" }) export const observe = Effect.fn("DeepAgentLearningLifecycleTrigger.observe")(function* ( db: DatabaseClient, diff --git a/packages/core/src/deepagent/lmn-events.ts b/packages/core/src/deepagent/lmn-events.ts index 1a0c8b8c..0c63fe54 100644 --- a/packages/core/src/deepagent/lmn-events.ts +++ b/packages/core/src/deepagent/lmn-events.ts @@ -63,6 +63,42 @@ export const PANEL_VERDICT = "panel.verdict" // producer (a dead delivery OF a dlq.alert never re-alerts). export const DLQ_ALERT = "dlq.alert" +// §9 S10 / FEAT-003 — a domain-pack pin change HAS happened: packsPin/packsUnpin publish this after +// the pinned-set file is durably written (the file write stays authoritative; the event is a +// post-commit observation, never a driver). It makes pack mutations EVENTED so downstream surfaces no +// longer have to wait for their next natural recomputation to notice a pin. +// +// BENEFIT BOUNDARY (deliberate, do not widen without review): +// - the federation four graphs do NOT read pack scope (the knowledge adapter only ever consumes +// released snapshots), so no federation-side reaction is expected from this event; +// - intended consumers are legacy retrieval-surface audit + UI notification — they subscribe as +// needed; this producer only guarantees the event is emitted and subscribable. +export const PACK_CHANGED = "pack.changed" + +// FEAT-006 — a Wiki/knowledge page HAS changed: WikiService.editKnowledge publishes this after a +// governed human edit lands (append-only new version, human provenance), and the §L execution +// archiver publishes it after persisting a session execution-archive page (payload carries +// `archive:true` + sessionID). Like PACK_CHANGED it is a post-commit observation, never a driver — +// the DocumentStore write stays authoritative; the event only makes the mutation EVENTED. +// +// IDEMPOTENCY (FEAT-003 pattern): idempotencyKey = wiki.page.changed:: — the +// doc+version pair is the natural dedup identity of the write; archive writes prefix the key with +// `archive:` so an archive page and a human edit of the same doc never collide. The bus dedupes on +// the key, so a redelivered write never double-publishes. +// +// SELF-LOOP GUARD: the event-driven archiver consumes ONLY ARCHIVE_TRIGGER_TYPES +// (session.completed / goal.completed) — wiki.page.changed is NOT an archive trigger (asserted in +// tests), so an archive write's own event can never re-trigger archival. The consumer-type filter +// isolates the producer→event→consumer loop by construction (same anti-self-cascade posture as +// DLQ_ALERT, which is guarded at the producer instead). +// +// BENEFIT BOUNDARY (deliberate, do not widen without review — mirrors the PACK_CHANGED contract): +// - the federation knowledge graphs only ever consume RELEASED snapshots, so NO federation-side +// reaction is expected from this event; +// - intended consumers are audit, UI notification, and future cache invalidation — they subscribe +// on demand; this producer only guarantees the event is emitted and subscribable. +export const WIKI_PAGE_CHANGED = "wiki.page.changed" + // §C/§D — a multi-agent subtask that could NOT auto-execute and needs a human: it exceeded the agent's // autonomy ceiling, or it is a level_5 suggestion_only action (never auto-runs). The Multi-Agent // Runtime publishes this so the §D2 Approval Queue surfaces it for a human decision (rather than the diff --git a/packages/core/src/deepagent/pinned-packs.ts b/packages/core/src/deepagent/pinned-packs.ts new file mode 100644 index 00000000..af979551 --- /dev/null +++ b/packages/core/src/deepagent/pinned-packs.ts @@ -0,0 +1,39 @@ +// FEAT-001: pinned domain packs (docs/34 §9 S10) persist per-workspace as a small JSON file under +// the gateway memory dir (`/memory/pinned-packs.json`). The GUI writes it via the HTTP +// handlers; the agent gateway reads it so pins actually shape retrieval. This module is the single +// fault-tolerant reader/writer shared by both sides — missing or corrupt files degrade to "no pins", +// never an error. It lives in core because the gateway (core) must read it and cannot import +// deepagent-code; the handlers (deepagent-code) import it from core. + +import path from "node:path" +import { mkdirSync, readFileSync, writeFileSync } from "node:fs" + +/** File name of the pinned-pack list inside the workspace memory dir. */ +export const PINNED_PACKS_FILE = "pinned-packs.json" + +/** Absolute path of the pinned-pack file for a given memory dir. */ +export const pinnedPacksFile = (memoryDir: string): string => path.join(memoryDir, PINNED_PACKS_FILE) + +/** + * Read the pinned pack ids from `/pinned-packs.json`. + * Fault-tolerant: a missing file, unreadable file, invalid JSON, or a non-array payload all yield + * an empty list (no pins). Non-string entries inside a valid array are dropped. + */ +export const readPinnedPacks = (memoryDir: string): string[] => { + try { + const raw = readFileSync(pinnedPacksFile(memoryDir), "utf8") + const parsed: unknown = JSON.parse(raw) + return Array.isArray(parsed) ? parsed.filter((entry): entry is string => typeof entry === "string") : [] + } catch { + return [] + } +} + +/** + * Write the pinned pack ids (deduplicated, sorted) to `/pinned-packs.json`, + * creating the memory dir when absent. + */ +export const writePinnedPacks = (memoryDir: string, ids: readonly string[]): void => { + mkdirSync(memoryDir, { recursive: true }) + writeFileSync(pinnedPacksFile(memoryDir), JSON.stringify([...new Set(ids)].sort(), null, 2), "utf8") +} diff --git a/packages/core/src/deepagent/plan-controller.ts b/packages/core/src/deepagent/plan-controller.ts index e7f8cba8..4d1a2af9 100644 --- a/packages/core/src/deepagent/plan-controller.ts +++ b/packages/core/src/deepagent/plan-controller.ts @@ -138,7 +138,19 @@ export const isLightweightMode = (mode: AgentMode | string): boolean => mode === // soft-blocked while the plan is stale; read/search/diagnosis tools always pass — otherwise a stale // plan could never be repaired (inspect first, then call `plan` to update, then continue). The // `plan` tool itself is never mutating, so it always passes the gate. -const MUTATING_TOOLS = new Set(["edit", "write", "patch", "apply_patch", "multiedit"]) +const MUTATING_TOOLS = new Set([ + "edit", + "write", + "patch", + "apply_patch", + "multiedit", + // FEAT-011 T5: the unified activity facade's MUTATING entry points — activity_start spawns a + // supervised background activity (task/goal/panel) and activity_control pause/resume/stop/steers + // one; both mutate execution state and must respect the stale-plan gate. activity_status / + // activity_result are read-only projections and stay unlisted. + "activity_start", + "activity_control", +]) const ALWAYS_ALLOWED_TOOLS = new Set(["read", "grep", "glob", "list", "ls", "search", "task", "webfetch"]) export const isMutatingTool = (toolName: string, command?: string | null): boolean => { diff --git a/packages/core/src/deepagent/profile-builder.ts b/packages/core/src/deepagent/profile-builder.ts new file mode 100644 index 00000000..54ff532a --- /dev/null +++ b/packages/core/src/deepagent/profile-builder.ts @@ -0,0 +1,168 @@ +// FEAT-002 (docs/34 §4.1/§5): the single authoritative ExtendedProblemProfile builder, lowered into +// core so the agent gateway (core) constructs the SAME profile it hands to knowledge retrieval and +// pack-snapshot recording — one activation authority per run. Previously this logic lived only in +// deepagent-code (profile-detector.ts), which the gateway cannot import; deepagent-code now +// re-exports from here. Pure logic + workspace-fact detection (package.json / README etc.); no +// runtime imports outside node builtins. + +import { existsSync, readFileSync } from "node:fs" +import path from "node:path" +import type { ExtendedProblemProfile } from "./domain-pack-registry" + +export type WorkspaceSignals = { + readonly cwd: string + readonly agentMode: "general" | "high" | "xhigh" | "max" | "ultra" + readonly scenarioMode: "direct" | "intelligence" + readonly userRequest: string + readonly languages?: readonly string[] + readonly frameworks?: readonly string[] + readonly packageScripts?: Readonly> + // Optional round signals from prior diagnosis / validation + readonly roundSignals?: readonly string[] + // User-pinned packs (from settings / HTTP API override) + readonly userOverrides?: readonly string[] +} + +const CODE_RISK_KEYWORDS = /security|auth|payment|billing|crypto|encrypt|secret|vuln|cve|pentest/i +const FINANCE_KEYWORDS = /finance|payment|billing|ledger|accounting|invoice|transaction|audit/i +const HEALTHCARE_KEYWORDS = /health|medical|patient|ehr|phi|hipaa|dicom|clinical/i +const PRIVACY_KEYWORDS = /gdpr|pii|personal.?data|privacy|consent|anonymi/i +const PHI_KEYWORDS = /phi|protected.health|patient.record/i +const READ_ONLY_QUERY_KEYWORDS = + /\b(query|count|list|show|read|inspect|status|diff|log|config|select|explain|grep|rg|find)\b|查一下|查询|统计|列出|显示|读取|看一下|多少个|有哪些|当前状态|日志里|配置里|数据库里/i +const MUTATION_KEYWORDS = + /\b(fix|implement|edit|modify|write|delete|deploy|migrate|migration|backfill|transaction|constraint|insert|update|alter|drop|restart|apply|remove)\b|修复|实现|修改|写入|删除|部署|迁移|更新|重启/i + +// Detect languages from the workspace (package.json / pyproject / go.mod / Cargo.toml etc.). +const detectLanguages = (cwd: string): string[] => { + const langs: string[] = [] + if (existsSync(path.join(cwd, "package.json"))) { + langs.push("javascript") + try { + const pkg = JSON.parse(readFileSync(path.join(cwd, "package.json"), "utf8")) + const deps = { ...pkg.dependencies, ...pkg.devDependencies } + if (deps["typescript"] || deps["@types/node"] || existsSync(path.join(cwd, "tsconfig.json"))) { + langs.push("typescript") + } + } catch { + langs.push("typescript") + } // assume TS on parse failure + } + if (existsSync(path.join(cwd, "pyproject.toml")) || existsSync(path.join(cwd, "setup.py"))) langs.push("python") + if (existsSync(path.join(cwd, "go.mod"))) langs.push("go") + if (existsSync(path.join(cwd, "Cargo.toml"))) langs.push("rust") + if (existsSync(path.join(cwd, "CMakeLists.txt"))) langs.push("cpp") + return [...new Set(langs)] +} + +// Detect frameworks from package.json deps. +const detectFrameworks = (cwd: string): string[] => { + const fw: string[] = [] + const pkgFile = path.join(cwd, "package.json") + if (!existsSync(pkgFile)) return fw + try { + const pkg = JSON.parse(readFileSync(pkgFile, "utf8")) + const deps = { ...pkg.dependencies, ...pkg.devDependencies } + if (deps["react"] || deps["react-dom"]) fw.push("react") + if (deps["solid-js"]) fw.push("solidjs") + if (deps["vue"]) fw.push("vue") + if (deps["svelte"]) fw.push("svelte") + if (deps["next"]) fw.push("next.js") + if (deps["express"] || deps["fastify"] || deps["hono"] || deps["@hono/core"]) fw.push("backend-framework") + if (deps["drizzle-orm"] || deps["prisma"] || deps["typeorm"]) fw.push("orm") + if (deps["bun"]) fw.push("bun") + if (deps["effect"]) fw.push("effect") + } catch { + /* skip */ + } + return [...new Set(fw)] +} + +// Derive code_domains from languages, frameworks, and user request. +const deriveCodeDomains = (langs: string[], frameworks: string[], request: string): string[] => { + const d: string[] = ["code"] + if (frameworks.some((f) => ["react", "solidjs", "vue", "svelte"].includes(f))) d.push("frontend") + if (frameworks.some((f) => f === "backend-framework")) d.push("backend") + if (langs.includes("cuda") || langs.includes("cpp") || /\b(kernel|gemm|cuda|gpu|sgemm|rocm)\b/i.test(request)) { + d.push("gpu_kernel") + if (!langs.includes("cpp")) langs.push("cpp") + } + if (langs.includes("python")) d.push("data") + if (READ_ONLY_QUERY_KEYWORDS.test(request) && !MUTATION_KEYWORDS.test(request)) { + d.push("query", "deterministic", "read_only") + } + return [...new Set(d)] +} + +// Extract risk markers from the user request + repo signals. +const extractRiskMarkers = (request: string, repoSignals: string[]): string[] => { + const m: string[] = [] + const hay = `${request} ${repoSignals.join(" ")}`.toLowerCase() + if (CODE_RISK_KEYWORDS.test(hay)) m.push("security") + if (FINANCE_KEYWORDS.test(hay)) m.push("finance") + if (HEALTHCARE_KEYWORDS.test(hay)) m.push("healthcare") + if (PRIVACY_KEYWORDS.test(hay)) m.push("pii", "privacy") + if (PHI_KEYWORDS.test(hay)) m.push("phi") + return [...new Set(m)] +} + +// Extract business_domains from request + risk markers. +const deriveBusinessDomains = (request: string, riskMarkers: string[]): string[] => { + const d: string[] = [] + if (FINANCE_KEYWORDS.test(request) || riskMarkers.includes("finance")) d.push("finance") + if (HEALTHCARE_KEYWORDS.test(request) || riskMarkers.includes("healthcare")) d.push("healthcare") + return d +} + +// Infer a rough task_kind from the user request (heuristic; good enough for pack scoring). +const inferTaskKind = (request: string): ExtendedProblemProfile["task_kind"] => { + const r = request.toLowerCase() + if (/\b(fix|bug|error|fail|broken|crash|traceback)\b/.test(r)) return "debug" + if (/\b(review|audit|check|inspect|scan)\b/.test(r)) return "review" + if (/\b(test|spec|coverage|unit|e2e|integration)\b/.test(r)) return "test" + if (/\b(migrat|upgrade|refactor|rename|move|convert)\b/.test(r)) return "migrate" + if (/\b(optim|improv|speed|perf|latency|memory|throughput)\b/.test(r)) return "optimize" + if (/\b(explain|document|describe|what|how|why)\b/.test(r)) return "explain" + if (/\b(deploy|release|ship|produc|operation)\b/.test(r)) return "operate" + return "implement" +} + +// Collect signals from the repo (directory names, config files, README snippet). +const collectRepoSignals = (cwd: string, userRequest: string): string[] => { + const signals: string[] = [userRequest.slice(0, 200)] + try { + const readmePath = ["README.md", "README.txt", "readme.md"].map((f) => path.join(cwd, f)).find(existsSync) + if (readmePath) signals.push(readFileSync(readmePath, "utf8").slice(0, 500)) + } catch { + /* skip */ + } + return signals +} + +// Build a full ExtendedProblemProfile from workspace signals. This is the production entry point +// called by the gateway/orchestrator before pack activation (docs/34 §5/§9 S5). FEAT-002: this is +// the ONE profile producer the gateway uses for both retrieval and the recorded pack snapshot. +export const buildProfile = (signals: WorkspaceSignals): ExtendedProblemProfile => { + const langs = signals.languages?.length ? [...signals.languages] : detectLanguages(signals.cwd) + const frameworks = signals.frameworks?.length ? [...signals.frameworks] : detectFrameworks(signals.cwd) + const repoSignals = collectRepoSignals(signals.cwd, signals.userRequest) + const codeDomains = deriveCodeDomains(langs, frameworks, signals.userRequest) + const riskMarkers = extractRiskMarkers(signals.userRequest, repoSignals) + const businessDomains = deriveBusinessDomains(signals.userRequest, riskMarkers) + + return { + scenario_mode: signals.scenarioMode, + agent_strength: signals.agentMode, + task_kind: inferTaskKind(signals.userRequest), + code_domains: codeDomains, + business_domains: businessDomains, + platforms: [], + languages: langs, + frameworks, + data_classes: riskMarkers.filter((r) => ["pii", "phi"].includes(r)), + risk_markers: riskMarkers, + repo_signals: repoSignals.slice(0, 3), + round_signals: signals.roundSignals ?? [], + user_overrides: signals.userOverrides ?? [], + } +} diff --git a/packages/core/src/deepagent/session-state.ts b/packages/core/src/deepagent/session-state.ts index 66f29717..9b13b640 100644 --- a/packages/core/src/deepagent/session-state.ts +++ b/packages/core/src/deepagent/session-state.ts @@ -112,6 +112,12 @@ export type SessionRunState = { // The last soft plan-gate warning claimed by a tool call. A stale latch can span many tool calls; // persisting its fingerprint prevents duplicate process-log warnings without touching history. lastPlanGateNudgeFingerprint: string | null + // FEAT-007: the locked active domain pack snapshot id of the LATEST gateway run for this session + // (`pack_snapshot:`, or the registry's empty/error sentinels). Recorded by the gateway at the + // run entry from the SAME activation knowledge retrieval constrains on (single authority, FEAT-002), + // and read by the session prompt loop to attribute each tool-request receipt's + // context_active_pack_set_snapshot_id. Null when no gateway run locked a snapshot for the session. + packSnapshotId: string | null } // V3.9 §D: session-state pointer to a running goal. The GoalLoop's GoalStatus (persisted in the @@ -175,6 +181,7 @@ export const getOrCreate = (sessionId: string, mode: AgentMode): SessionRunState completedAt: null, lastAdmissionUserMessageId: undefined, lastPlanGateNudgeFingerprint: null, + packSnapshotId: null, } sessions.set(sessionId, state) saveToDisk() @@ -679,6 +686,8 @@ function normalizeState(state: SessionRunState): SessionRunState { panelRounds: state.panelRounds ?? null, activeGoal: state.activeGoal ?? null, lastPlanGateNudgeFingerprint: state.lastPlanGateNudgeFingerprint ?? null, + // Backfill: sessions persisted before FEAT-007 have no pack snapshot attribution → null. + packSnapshotId: state.packSnapshotId ?? null, } // PR-4 migration cleanup: the legacy `suppressedFingerprints: string[]` field is carried through by // the `...state` spread above. Once its contents are migrated into `suppressedValidations`, drop the diff --git a/packages/core/src/event.ts b/packages/core/src/event.ts index c9e15da2..3bb80b07 100644 --- a/packages/core/src/event.ts +++ b/packages/core/src/event.ts @@ -128,6 +128,44 @@ export type SerializedSnapshotChunk = { readonly chunkHash: string } +// RISK-003 ⑤ (BUG-407-010 §9.2/§11.4): the portable, importable aggregate snapshot body. +// The row/chunk transfer protocol (snapshotRows/snapshotChunks → stage* → importSnapshot) is the +// durable DB-to-DB path used by warp/sync; this bundle serializes the same authority into one +// JSON body (chunk data base64) that can be persisted as evidence and atomically imported into an +// empty copy. `bundleHash` covers the snapshot header, the row manifest, and every chunk body. +export const SNAPSHOT_BUNDLE_FORMAT = "aggregate-snapshot-bundle.v1" + +export const SnapshotBundle = Schema.Struct({ + format: Schema.Literal(SNAPSHOT_BUNDLE_FORMAT), + snapshot: Schema.Struct({ + snapshotID: Schema.String, + aggregateID: Schema.String, + throughSeq: NonNegativeInt, + syncSeq: NonNegativeInt, + codec: Schema.String, + schemaVersion: NonNegativeInt, + snapshotHash: Schema.String, + body: Schema.Record(Schema.String, Schema.Unknown), + ownerID: Schema.optional(Schema.String), + createdAt: NonNegativeInt, + }), + rows: Schema.Array( + Schema.Struct({ + snapshotID: Schema.String, + rowIndex: NonNegativeInt, + tableName: Schema.String, + rowKey: Schema.String, + rowHash: Schema.String, + rowBytes: NonNegativeInt, + chunkCount: NonNegativeInt, + chainHash: Schema.String, + }), + ), + chunks: Schema.Record(Schema.String, Schema.Array(Schema.String)), + bundleHash: Schema.String, +}) +export type SnapshotBundle = typeof SnapshotBundle.Type + export type SnapshotProjectionRow = { readonly cursor: string readonly tableName: string @@ -432,6 +470,8 @@ export interface Interface { row: SerializedSnapshotRow, chunks: readonly SerializedSnapshotChunk[], ) => Effect.Effect + readonly exportSnapshotBundle?: (input: { readonly snapshotID: string }) => Effect.Effect + readonly importSnapshotBundle?: (bundle: string) => Effect.Effect readonly compact: (input: { readonly aggregateID: string readonly throughSeq: Cursor @@ -1862,6 +1902,135 @@ export const layerWith = (layerOptions?: LayerOptions) => }), { behavior: "immediate" }).pipe(Effect.orDie) } + function exportSnapshotBundle(input: { readonly snapshotID: string }) { + return Effect.gen(function* () { + const header = yield* db + .select({ + snapshotID: EventSnapshotTable.snapshot_id, + aggregateID: EventSnapshotTable.aggregate_id, + throughSeq: EventSnapshotTable.through_seq, + syncSeq: EventSnapshotTable.sync_seq, + codec: EventSnapshotTable.codec, + schemaVersion: EventSnapshotTable.schema_version, + snapshotHash: EventSnapshotTable.snapshot_hash, + body: EventSnapshotTable.body, + ownerID: EventSnapshotTable.owner_id, + createdAt: EventSnapshotTable.created_at, + }) + .from(EventSnapshotTable) + .where(eq(EventSnapshotTable.snapshot_id, input.snapshotID)) + .get() + .pipe(Effect.orDie) + if (!header) + return yield* Effect.die( + new InvalidSyncEventError({ type: "snapshot", message: `Snapshot ${input.snapshotID} not found` }), + ) + const snapshot: SerializedSnapshot = { + snapshotID: header.snapshotID, + aggregateID: header.aggregateID, + throughSeq: header.throughSeq, + syncSeq: header.syncSeq, + codec: header.codec, + schemaVersion: header.schemaVersion, + snapshotHash: header.snapshotHash, + body: header.body, + ...(header.ownerID ? { ownerID: header.ownerID } : {}), + createdAt: header.createdAt, + } + const rows: SerializedSnapshotRow[] = [] + let after = -1 + while (true) { + const page = yield* snapshotRows({ snapshotID: input.snapshotID, after }) + rows.push(...page) + if (page.length === 0) break + after = page[page.length - 1]!.rowIndex + } + const chunks: Record = {} + for (const row of rows) { + const encoded: string[] = [] + let chunkAfter = -1 + while (true) { + const page = yield* snapshotChunks({ rowHash: row.rowHash, after: chunkAfter }) + if (page.length === 0) break + for (const chunk of page) encoded.push(chunk.data.toString("base64")) + chunkAfter = page[page.length - 1]!.chunkIndex + } + chunks[row.rowHash] = encoded + } + const bundle = { format: SNAPSHOT_BUNDLE_FORMAT, snapshot, rows, chunks } + const text = JSON.stringify({ ...bundle, bundleHash: hashJson(bundle) }) + if (Buffer.byteLength(text) > SNAPSHOT_MAX_TOTAL_BYTES) + return yield* Effect.die( + new InvalidSyncEventError({ + type: "snapshot", + message: `Snapshot ${input.snapshotID} bundle exceeds the transfer budget`, + }), + ) + return text + }) + } + + function importSnapshotBundle(bundle: string) { + return Effect.gen(function* () { + if (Buffer.byteLength(bundle) > SNAPSHOT_MAX_TOTAL_BYTES) + return yield* Effect.die( + new InvalidSyncEventError({ type: "snapshot", message: "Snapshot bundle exceeds the transfer budget" }), + ) + const decoded = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)(bundle) + const parsed = decoded._tag === "Some" ? decoded.value : undefined + const candidate = Schema.decodeUnknownOption(SnapshotBundle)(parsed) + if (candidate._tag !== "Some") + return yield* Effect.die( + new InvalidSyncEventError({ + type: "snapshot", + message: `Snapshot bundle is not a valid ${SNAPSHOT_BUNDLE_FORMAT} body`, + }), + ) + const value = candidate.value + const { bundleHash, ...rest } = value + if (hashJson(rest) !== bundleHash) + return yield* Effect.die( + new InvalidSyncEventError({ type: "snapshot", message: "Snapshot bundle hash mismatch" }), + ) + const snapshot: SerializedSnapshot = { + snapshotID: value.snapshot.snapshotID, + aggregateID: value.snapshot.aggregateID, + throughSeq: value.snapshot.throughSeq, + syncSeq: value.snapshot.syncSeq, + codec: value.snapshot.codec, + schemaVersion: value.snapshot.schemaVersion, + snapshotHash: value.snapshot.snapshotHash, + body: value.snapshot.body as Record, + ...(value.snapshot.ownerID ? { ownerID: value.snapshot.ownerID } : {}), + createdAt: value.snapshot.createdAt, + } + // Stage bounded transfer pages (rows, then per-row chunks) before the authoritative + // import — the exact protocol the wire transfer uses, so staging CAS guards apply. + for (let index = 0; index < value.rows.length; index += SNAPSHOT_TRANSFER_ROWS) { + yield* stageSnapshotRows(snapshot, value.rows.slice(index, index + SNAPSHOT_TRANSFER_ROWS)) + } + for (const row of value.rows) { + const encoded = value.chunks[row.rowHash] + if (!encoded || encoded.length !== row.chunkCount) + return yield* Effect.die( + new InvalidSyncEventError({ + type: "snapshot", + message: `Snapshot bundle row ${row.rowIndex} chunks are missing`, + }), + ) + const chunks = encoded.map((data, chunkIndex) => { + const buffer = Buffer.from(data, "base64") + return { rowHash: row.rowHash, chunkIndex, data: buffer, chunkHash: Hash.sha256(buffer) } + }) + for (let index = 0; index < chunks.length; index += SNAPSHOT_TRANSFER_CHUNKS) { + yield* stageSnapshotChunks(snapshot, row, chunks.slice(index, index + SNAPSHOT_TRANSFER_CHUNKS)) + } + } + yield* importSnapshot(snapshot) + return snapshot + }) + } + function compact(input: { readonly aggregateID: string readonly throughSeq: Cursor @@ -2629,6 +2798,8 @@ export const layerWith = (layerOptions?: LayerOptions) => snapshotChunks, stageSnapshotRows, stageSnapshotChunks, + exportSnapshotBundle, + importSnapshotBundle, backfillSyncIndex, remove, claim, diff --git a/packages/core/src/permission/mutating.ts b/packages/core/src/permission/mutating.ts new file mode 100644 index 00000000..4b20887d --- /dev/null +++ b/packages/core/src/permission/mutating.ts @@ -0,0 +1,15 @@ +// Shared mutating-permission set used by every read-only approval mode. +// +// Tools that write, execute, or otherwise mutate state — denied in read-only +// mode. Everything else (read/glob/grep/list/lsp/webfetch/websearch/ +// todowrite/…) is left to the normal permission flow so the agent can still +// inspect the workspace. +// +// Consumers: the GUI directory read-only mode (packages/app) and the CLI +// `run --permission-mode read-only` auto-responder (packages/deepagent-code) +// must agree on this set, so it lives in core. +export const MUTATING_PERMISSIONS = new Set(["edit", "write", "patch", "bash", "task", "external_directory"]) + +export function isMutatingPermission(permission: string) { + return MUTATING_PERMISSIONS.has(permission) +} diff --git a/packages/core/src/session/compaction.ts b/packages/core/src/session/compaction.ts index 99765f73..bc6b458f 100644 --- a/packages/core/src/session/compaction.ts +++ b/packages/core/src/session/compaction.ts @@ -11,7 +11,10 @@ import { Token } from "../util/token" const DEFAULT_BUFFER = 20_000 const DEFAULT_KEEP_TOKENS = 8_000 -const TOOL_OUTPUT_MAX_CHARS = 2_000 +// UPD-005: single source of truth for the compaction-side tool-output truncation budget. +// deepagent-code/src/session/compaction.ts imports this constant (it used to keep a duplicate +// 2_000 copy); keep the two call sites in sync by editing ONLY this definition. +export const TOOL_OUTPUT_MAX_CHARS = 2_000 const SUMMARY_OUTPUT_TOKENS = 4_096 const SUMMARY_TEMPLATE = `Output exactly the Markdown structure shown inside