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